Skip to main content

OS Optics (os-optics)

OS Optics is OpenSci's local optical design engine. Design an optical system from OpenSci's public optical catalog, simulate it with a deterministic physics engine, look at it in 3D, and get a structured Local Validation record -- all on your own computer. When the design is ready to buy, the same package takes it through OpenSci's commercial service: the Customer Purchase BOM and prices, the Experimental Lab Assembly, the Order, and after payment the Detailed Assembly Drawing. This is OpenSci "Mode A", the headless Python route (the browser Designer, "Mode B", runs the same engine in a web page); the product site is https://opensci-optics.com/.

  • Pure Python for Python 3.11 or newer (3.11, 3.12, 3.13, 3.14) on Windows, Linux and macOS; depends on numpy, scipy, pydantic>=2 and shapely (the protected extra adds cryptography). No compiler, GPU or account is needed to design.
  • Same input + same engine version + same numerical settings = same result within the declared tolerance.
  • Anything outside the published models is reported (model_scope_issues, Engine issues and targets of status OUT_OF_MODEL_SCOPE, UNKNOWN targets) and the run is never PASS: its status is at least OUT_OF_MODEL_SCOPE, or FAIL when a target also fails.
  • You, or your own Agent, operate this package. OpenSci does not provide, host or embed an Agent; any coding Agent that can run Python or a shell can drive the commands and functions below. Every command prints JSON.

Install

With uv (it selects or downloads a compatible Python by itself):

uv tool install "os-optics[protected]"
os-optics doctor

or run without installing: uvx --from "os-optics[protected]" os-optics doctor.

With pip, in a virtual environment made from Python 3.11 or newer:

python --version                                  # 3.11 or newer
python -m venv .venv
.venv/bin/python -m pip install "os-optics[protected]"     # Windows: .venv\Scripts\python -m pip install ...
.venv/bin/os-optics doctor                                      # Windows: .venv\Scripts\os-optics doctor

If pip reports "requires a different Python" or "No matching distribution found for os-optics", the interpreter is older than 3.11: use the uv command above, or create the environment from a newer Python.

Up to version 0.9.0 OS Optics was published as opensci-engine (import opensci_engine, command opensci-engine). From 0.10.0 it is published as os-optics: install it with the commands above (also where opensci-engine is installed; the two share no file), import os_optics and run os-optics.

When the service asks for a newer engine. A submission validated with an engine version the OpenSci service does not accept is refused with ENGINE_VERSION_NOT_ACCEPTED (recovery UPGRADE_ENGINE_AND_RERUN). Upgrade, then move each saved Project to the current catalog and run it again:

python -m pip install --upgrade "os-optics[protected]"
os-optics catalog sync
os-optics project update-catalog project.json --out project.json   # add --include-changed if it reports "unverified"
os-optics project run project.json --out-dir run
os-optics resolve run

When update-catalog lists components as unverified (their product data cannot be compared with the current release), it keeps them unchanged: add --include-changed to move them (status UPDATED_UNVERIFIED), then review the new Local Validation before you submit.

os-optics doctor (or python -m os_optics doctor) prints one JSON report: the engine version, Python and platform, package versions, whether the [protected] extra is installed, the local public optical catalog, and whether OPENSCI_API_URL and an Account token are set up. ready.local_design: true (exit code 0) means you can start designing; next_steps lists the exact commands for anything missing. The token itself is never shown. Doctor is local and offline: it does not contact OpenSci or test the connection (os-optics catalog check asks the catalog service, os-optics account status checks your token with the service). On a fresh install catalog.status is DOWNLOAD_ON_FIRST_USE, a warning, not a failure: the catalog is downloaded by the first command that needs it, or now with os-optics catalog sync. The [protected] extra is needed to open the Experimental Lab Assembly and the Assembly Detail of a resolution; the catalog, Projects, simulation, Local Validation and the 3D view work without it.

The public optical catalog: synchronized, versioned, offline-first

The package does not contain the product catalog. OpenSci publishes catalog releases (catalog_version, e.g. 2026-09-30.3) on its service, and the engine keeps verified copies in a local catalog store, so new products and catalog corrections arrive without a new engine release:

  • First run. The first command that needs the catalog downloads the current release (a few MB, verified by SHA-256) into ~/.opensci/catalog/ (or OPENSCI_CATALOG_DIR); one line on stderr names the release, its size and the service it came from (standard output stays JSON). After that the engine checks the service at most once an hour with a small conditional request and downloads only a release it does not have (OPENSCI_CATALOG_CHECK_INTERVAL in seconds; 0 turns automatic checks off). Offline, the stored release is used.
  • Explicit control. os-optics catalog status (local, no network: the last contact with the service and its time), catalog check (ask the configured service now; when it cannot be reached, up_to_date is null rather than a stale answer), catalog sync (download the current release and make it active), catalog verify (re-check the stored files). From Python: from os_optics import catalog; catalog.status(); catalog.check(); catalog.sync(); catalog.verify().
  • Which recovery command. catalog sync downloads the service's current release and makes it active (the usual fix); catalog sync --release V fetches one release the service still distributes (it becomes active only when no release is active); catalog use V selects a release already stored on this computer; catalog import FILE adds a release file on a computer without access to the service.
  • Offline computers. Copy a catalog release file to the computer and run os-optics catalog import public_optical_catalog.json.
  • Several programs at once. Agents and commands on one computer may share the catalog store; its changes are serialized by a lock file, so no release or setting is lost. If the store's index is lost, it is rebuilt from the stored releases without guessing which one was active: the next check selects the service's current release, and offline the engine reports CATALOG_ACTIVE_UNKNOWN until you choose one with os-optics catalog use <catalog_version> (or catalog sync / catalog import).
  • Reproducible designs. Each catalog component of a Project pins the catalog_version it was designed with, and a run uses exactly that release (fetched once if it is missing; on a computer with no stored release it also becomes the active release, and an active release is never replaced by it). os-optics project update-catalog project.json --out project.json moves a Project to the current release: components whose product is unchanged are re-pinned; changed, withdrawn and unverified products (their old release is not on this computer and cannot be obtained) are listed with next_steps; --include-changed moves changed and unverified ones too (status UPDATED_UNVERIFIED: run the Project again and review the result). Run an updated Project again before you submit it.
  • The service is OPENSCI_CATALOG_URL, else OPENSCI_API_URL, else https://opensci-optics.com/. If the service publishes a release in a newer catalog format than this engine reads, catalog status / doctor say ENGINE_UPGRADE_REQUIRED and the stored release stays in use until you upgrade.
os-optics catalog sync                     # first run (or let the first design command do it)
os-optics catalog status                   # active release, stored releases, last check
os-optics catalog import catalog.json      # an offline computer: use a release file you copied
os-optics catalog use 2026-09-30.3         # make a stored release the active one (no network)

Quick start

os-optics catalog list --category LENS --wavelength-nm 780 --limit 20   # products usable at 780 nm (paged rows)
os-optics catalog list --category LENS --focal-length-mm -50 --focal-tolerance-mm 5   # negative lenses near -50 mm
os-optics catalog show OS-OPT-...                            # one product: specifications, surfaces, band, geometry
os-optics project example --kind beam_expander > project.json   # a complete two-lens reference design to copy and edit
os-optics project run project.json --out-dir run             # simulate; exit code 0 PASS, 1 FAIL, 2 UNKNOWN, 3 OUT_OF_MODEL_SCOPE
os-optics view run                                           # the 3D Optical Simulation View in a local browser page
os-optics view run --snapshot shots                          # or PNG views + scene_summary.json, without a browser
os-optics project verify run                                 # re-check the run directory's hashes and bindings

Reference designs (project example --kind ...): focus (a single lens), folded (a fold mirror and a lens), beam_expander (two-lens telescope), relay_4f, folded_expander, dichroic_routing (two wavelengths split by a dichroic mirror), polarization (waveplate + polarizer), two_beam_interference (one laser split into two arms that cross on a plane: targets on the fringe period, the intensity balance, the polarization overlap and the coherence length), two_color_dichroic (one source of two spectral lines whose lines a dichroic mirror sends to two detectors) and dichroic_combiner (two lasers combined onto one beam by a dichroic mirror, one meeting its coated face and the other its back face). Each is built from the active catalog release by query.

Multi-line sources: a source that emits several lines (a 532 + 1064 nm laser, a multi-line ion laser, an RGB source) lists them as spectral_lines ([{"wavelength_nm": 532, "power_w": 0.001}, ...], distinct wavelengths) instead of wavelength_nm and power_w. Every line is traced as its own beam with its own power, dispersion and coating response; each observation reports every line's beam with its wavelength and its power_by_wavelength; the power budget is per line; a target selects a line with wavelength_nm. Waist, M², polarization and coherence apply to every line.

Interference: two beams of one source interfere within its coherence length. Declare it on the source model, as linewidth_nm (spectral FWHM) or coherence_length_mm (one of the two). Without it, fringe visibility, peak irradiance and the coherence metrics are not computed, and a target on one of them is UNKNOWN; fringe period, path difference and overlap do not need it. Beams of two different source components are mutually incoherent, and so are the different lines of one multi-line source; a linewidth gives each line the coherence length of its own wavelength. Two beams of the same wavelength from different source components form no lasting fringe: a fringe_visibility target on them reads 0. Beams of different wavelengths, including the lines of one source, are not paired. The coherence follows the beams' relative group delay, which is known only where both beams cross the same elements and coatings whose own delay the design does not state: an element without a thickness (an ideal or tabulated thin lens, a splitter plate, a polarizer, a retarder, an attenuator, an aperture that attenuates) and a coating without a stated phase -- also met beyond the critical angle -- delay light by an unknown amount (arrivals[].group_delay_unknown_at), a coating met obliquely, a retarder and a polarizer that passes light along both axes by an amount that may differ between polarizations (the record carries the beam's state there). Beams that cross such passes differently -- the two arms of an interferometer through a coated splitter, coated mirrors or a thin filter in one arm, or two beams in different polarization states through a common retarder or oblique coating -- have no known degree of coherence: the same metrics are then not computed (COHERENCE_UNDEFINED), and the exposure bounds take them as fully coherent. Model the element with its thickness and material, or state the coating's phases (constant over wavelength), to make it known. The same passes leave the phase between the two beams open: the trace gives a coating that states no phase a default one and an element without a thickness none, and neither is a statement about the part. Where one beam makes such a pass and the other does not, their phase_difference_rad is not computed (PHASE_UNDEFINED) and a target on it is UNKNOWN; passes both beams make alike cancel, and a coating that states its phases gives the value (a phase table states the phase without its group delay: such a pass is listed in group_delay_unknown_at ending in #phase). The fringe period and the beams' spatial overlap do not depend on it. A coating may also state a bound of its group delay, group_delay_bound_fs (catalog coatings from catalog schema 3.2, per coating; a declared coating the same way). Where every pass two beams do not share has such a bound, their relative group delay is known within the sum B of those bounds, and their degree of coherence within δ = 2B / (√π L_c) of its value at the traced delay, whatever the coatings' dispersion within the bounds. When δ is at most 0.01, the fringe visibility, peak irradiance and coherence factor and margin are given at the traced delay together with the range the bounds leave (group_delay_bound_mm, coherence_factor_min / coherence_factor_max), and a target on one of them is met or not met only when its whole range is; a larger δ leaves them not computed (COHERENCE_UNDEFINED). A Mach–Zehnder of two plate splitters and two dielectric mirrors whose coatings state 1000 fs (splitter and mirror coatings) and 50 fs (the plates' anti-reflection faces) has B = 6100 fs, 1.83 mm of group path: a coherence length of 207 mm or more decides its visibility. A pulsed source keeps such pairs undetermined. The bound says how the phase between the two beams changes across the line, not its value: the phase difference, and the power of beams that overlap on a plane (coherent_power_W, superpositions[].total_power_W, combined_power_collinear_W), depend on the reflection and transmission phases of the coatings they meet differently. Where those coatings state their phases, the coherent power is given with the range the bounds leave (total_power_min_W / total_power_max_W); where they do not, it has no value (PHASE_UNDEFINED), only its range over every phase -- for the Mach–Zehnder above, from 0 to the sum of the two arms' fields in phase -- and a target on it is decided only when that whole range meets or misses it. An oblique coating of unstated phase that a beam meets in a state mixing s and p also changes its polarization by an unstated amount: the visibility is then not computed. Send s- or p-polarized light to such coatings, or state their phases.

The same from Python:

import tempfile
from os_optics.local import example_project_of_kind, load_public_catalog, run_optical_project, verify_run_artifacts
from os_optics.viewer import render_snapshots

catalog = load_public_catalog()                              # the active catalog release (local catalog store)
print(catalog.catalog_version, len(catalog.products), "products")
lens = catalog.select(category="LENS", form="PLANO_CONVEX_LENS", wavelength_nm="780")[0]
print(lens.optical_sku, "|", lens.display_name, "|", lens.display_name_zh_cn)

project = example_project_of_kind("beam_expander")           # a ProjectDocumentV2: source -> L1 -> L2 -> output plane
run = run_optical_project(project, output_dir=tempfile.mkdtemp())   # or a JSON document / JSON text / file path
print(run.status.value, [(t.target_id, t.status.value, round(t.value, 3)) for t in run.result.targets])
print(run.local_validation.validation_status.value, run.local_validation.input_hash.digest[:12])
assert verify_run_artifacts(run.artifacts.directory) == ()
render_snapshots(run.artifacts.directory, "shots")           # iso / top / front / side PNGs, scene_summary.json, legend.md

Computation budgets

The Project stores its computation settings in compute_budget. The defaults are 64 physical interactions per path, 512 paths, 2,048 total trace steps and 256 interference pairs per observation. Increase the relevant budget for a longer optical chain, more source lines or branching paths, or more pair calculations. Free propagation and observation planes do not consume physical interactions; they do consume trace steps. The primary trace and auxiliary light probes each use the stated path and step allowances; the execution result identifies which phase exhausted its budget.

Use the same edit operation in Python, the CLI or a workspace:

from os_optics.local import example_project_of_kind, json_api
project = example_project_of_kind("beam_expander")
edited = json_api.update_project(project, [{
    "op": "set_compute_budget",
    "compute_budget": {"max_interactions_per_path": 800, "max_paths": 4096,
                       "max_trace_steps": 12000, "max_interference_pairs": 1024},
}])
assert edited["ok"], edited
project = edited["project"]
print(edited["compute_budget"])  # all four effective settings, including defaults

Save the operation array as budget-ops.json, then run:

os-optics project update project.json --ops budget-ops.json -o project.json
os-optics project check project.json
os-optics workspace update-project system.json --ops budget-ops.json

set_compute_budget replaces the budget block; omitted fields use defaults. Set compute_budget to null to reset it. capabilities lists the defaults and accepted fields. The settings persist through workspace export, are included in the run manifest and input identity, and changing them makes older runs historical. Each budget is a positive integer up to 9,007,199,254,740,991, the largest integer preserved exactly by both Python and browser JSON; this representation limit is not a suggested workload. Increase one limit at a time and inspect execution completeness and artifact size. The defaults keep ordinary runs bounded; larger path and step budgets can produce large ray files and 3D scenes. View selection changes only displayed rays: it never reduces the physical pupil sampling or the saved ray data.

run.status is the diagnostic summary, run.result the full Engine result, run.local_validation the LocalValidationRecordV1, run.optical_view the 3D scene and run.artifacts.paths the written files. One run writes one directory:

File What it is
manifest.json start here: validation / simulation status, per-target status, Project / catalog / Engine identities and hashes, retained DOFs, the file list with SHA-256, and the binding checks
project.json the ProjectDocumentV2 that was run
engine_request.json the Engine ValidationRequest lowered from it (os-optics validate can re-run it)
simulation_result.json the Engine ValidationResult: target values and statuses, beams at each observation plane, paths, power budget, issues
ray_data.json every chief and pupil ray the Engine traced: point, direction and optical path length at each surface reached (read it with os_optics.results)
local_validation.json the LocalValidationRecordV1: Project hash, Engine input / result hashes, Engine version and build id, optical SKUs + catalog version + simulation model, retained DOF intent, target outcomes, simulation and validation status
optical_scene.json the Optical Simulation View as JSON (os-optics schema optical-scene)
optical_scene.glb the same view as glTF 2.0: os-optics view shows it; any glTF viewer reads it too (nodes carry part classes and ids)

All files come from one Engine execution: the scene and the record carry the same Engine input and result hashes, and verify checks that. The view shows the optical elements with their design geometry, the sources, the Engine-traced chief and pupil rays, and the observation / target planes with the beam footprints. The rays are real rays traced through every published surface (the drawn pupil rays are the Engine's outer equal-power ring, just outside the 1/e² radius), so they show the lenses' real aberrations; the Gaussian beam numbers (waist, radius) are paraxial, in simulation_result.json and the footprints. Where the rays spread more than twice as wide as the Gaussian beam allows (an aberration-dominated focus), the Engine reports that beam's Gaussian numbers as OUT_OF_MODEL_SCOPE (GAUSSIAN_ABERRATION_DOMINATED) and pupil_rms_radius_mm gives the rays' own spot.

Reading the outcome. run.result.outcome.execution reports whether the calculation completed or stopped with partial results. The local validation record and result queries also incorporate post-trace model applicability: affected targets have assessment INDETERMINATE, and affected observations have quantity state OUT_OF_MODEL_SCOPE, with the specific scope issues retained. Other target assessments are MET, UNMET or NOT_APPLICABLE; no declared targets means no requirement assessment. Observation quantity states distinguish known power, known zero, a supported bound, unknown power and unsupported power. The raw Engine values and result hash remain unchanged by local applicability checks. A missed optional target does not by itself prevent purchasing.

total_power_W counts traced beam contributions. If light the trace does not follow may also arrive (a ghost reflection beside the main beam, omitted faint light), power_complete is false, untraced_power_W states the followed ghost light, and power_upper_bound_W records the supported upper bound of the total beam power (not the separate coherent-power metric): the same upper end a power_W target on the plane is graded on. A null bound means no finite upper bound is established; older results may omit the field. A traced contribution of 0 W with a positive upper bound is BOUNDED, not KNOWN_ZERO. Removing an optional target or changing its threshold does not change this quantity evidence.

The compatible aggregate status summarizes diagnostic severity. FAIL: a target was missed or the beam was lost -- see the targets and issues. UNKNOWN: something could not be evaluated. OUT_OF_MODEL_SCOPE: the question is outside what the published models answer (for example a wavelength outside a product's band or in a dichroic's transition gap, a coating met beyond its published angle of incidence by the chief ray, part of the beam falling on an element its chief ray misses, light leaving or crossing an element through its edge, or an aberration-dominated focus; run.model_scope_issues, the Engine issues and manifest.json say which element, surface and value) -- never treat it as a pass. A run used a model outside its validity when model_scope_issues is not empty or any Engine issue or target has status OUT_OF_MODEL_SCOPE (a tabulated thin lens reports its table's domain as Engine issues, also listed in model_scope_issues). The status is the worst of all outcomes (FAIL > OUT_OF_MODEL_SCOPE > UNKNOWN > WARNING > PASS), so a failing target makes such a run FAIL, and a target outside the model's band can read PASS. results targets RUN answers it with every target outcome: in_model_scope and each finding in out_of_model_issues. A Project that cannot be simulated as given is refused before anything runs (LocalRunError; the CLI prints {"status": "ERROR", "problems": [...]} and exits 65, and writes no run directory) with structured problem codes such as OPTICAL_SKU_NOT_PUBLISHED, PRODUCT_ROLE_MISMATCH or SOURCE_MODEL_MISSING. ERROR is not a result status: there is no result to report.

Writing your own Project. Start from os-optics project example (or example_project().payload() in Python) and edit it; os-optics schema project prints the JSON Schema and print(os_optics.local.__doc__) explains every part (design frame, poses, sources, DOFs, observations, targets, where to expect the focus). In short: components are optical elements -- catalog products by OS-OPT-* SKU and your own sources with an explicit Gaussian source_model (an extended source is model_kind EXTENDED_EMITTER: an emitting_surface, an angular_law, total radiant power and sampling, with collection fractions and binned irradiance in the results, see docs/RESULTS_AND_EXPORT.md; a beam is GAUSSIAN_BEAM: wavelength and power, or several spectral_lines; 1/e² waist radius and position, M², polarization with its angle from the source's local +X toward local +Y; for a pulsed source pulse: repetition_rate_hz, pulse_duration_s -- the FWHM of the intensity, duration_definition "FWHM" -- and optionally total_pulses, the powers then average powers; without pulse the source is continuous); each has a nominal_pose (mm, unit quaternion w, x, y, z; local +Z is the optical axis, a catalog lens frame starts at its first surface vertex and light enters from local -Z) and optional retained_dofs (Tx..Rz, MANUAL or NUMERICAL: which degrees of freedom stay adjustable, by hand or motorized). The design frame is right-handed, in mm, with +Z up; the examples lay the beam along +X. observations are planes where beams are measured (every observation reports its beams in simulation_result.json, target or not); targets put a metric (os_optics.local.target_metrics(); help() on it gives units and sign conventions) on an observation with at least one bound. A target's parameters take only the keys its metric lists in os-optics results metrics (parameters.path_id, a string such as "SRC#0", and parameters.crossing_index, an integer; two-beam path_id_a / path_id_b / crossing_index_a / crossing_index_b; dose_mj_per_cm2, a number, for estimated_exposure_time_s): any other key or value type is refused (TARGET_PARAMETER_INVALID, exit 65). Ids you choose (instance_id, observation_id, target_id) are copied verbatim into every artifact. Mounts, posts, stages and the breadboard are not part of a design: OpenSci configures them for each design at commercial resolution.

Edit, check and run again. project update applies edit operations to a Project, the same vocabulary as the browser Designer's updateProject (set_component_pose, set_source_model {instance_id, changes}, set_target {target_id, changes}, set_observation, add_* / remove_*, the declared-model operations, ...; in changes a null removes a field) plus transform (a rigid move of selected components about an explicit frame and pivot, as workspace transform computes it). All operations and the Project check pass, or nothing is written:

cat > ops.json <<'EOF'
[{"op": "set_source_model", "instance_id": "SRC", "changes": {"wavelength_nm": 532}},
 {"op": "transform", "selector": {"ids": ["L2"]}, "translation_mm": [0, 0, 5], "frame": {"component": "L2"}}]
EOF
os-optics project check project.json                  # identity (project_hash, engine_input_hash), problems, warnings
os-optics project update project.json --ops ops.json --expect-hash HASH -o edited.json
os-optics project run edited.json --out-dir run2
os-optics results targets run2                        # every target: value, unit, bounds, status; in_model_scope
os-optics results freshness run --project edited.json # HISTORICAL (PHYSICS_CHANGED): the design's optics, sources, planes or compute_budget changed
os-optics capabilities                                # versions, metrics with units, operations, result queries (JSON)

--expect-hash refuses the update (PROJECT_CHANGED, with the current project_hash) when the Project is no longer the one the operations were written for. A refused operation names its index and code (PROJECT_OP_INVALID: NOT_FOUND, DUPLICATE_ID, BAD_ARGUMENT, IN_USE, UNKNOWN_OP, TRANSFORM_REFUSED); a refused result lists the check's problems (PROJECT_REJECTED). Applying the same absolute operations again changes nothing (changed: false), and adding an item identical to the one already present is reported in already_present. transform is relative, and a list that adds an item and then changes or removes it ([add_target X, set_target X ...]) is refused when applied again to its own result (DUPLICATE_ID / NOT_FOUND), so after a lost response retry with --expect-hash: PROJECT_CHANGED then reports the current project_hash; if the first application took effect, it is the hash the lost reply carried. The check's advisory warnings name what decides part of the outcome before a run: a target whose unit is not its metric's (TARGET_UNIT_MISMATCH: the run gives that target no value; its status is UNKNOWN, and a target the run cannot evaluate at all keeps that outcome) and a source line outside the wavelengths a component's model is characterized for (SOURCE_WAVELENGTH_OUTSIDE_COMPONENT_BAND: if that light meets the component, the run reports it in model_scope_issues; for a tabulated thin lens also as an Engine issue in issues, and details.reported_in is then engine_issues). From Python: json_api.update_project(project, ops, {"expected_project_hash": h}), json_api.check_project(project)["warnings"], json_api.run_freshness(project, "run") and load_run("run").targets(). Tools built on these calls -- parameter sweeps, run comparisons, plots and reports --, engine-reproducible cases and agent skills are in os-optics-kit (https://github.com/JunLin615/os-optics-kit): python -m pip install os-optics-kit, then os-optics-kit skills list.

Scientific results and export

A run directory is the complete scientific record of one Engine execution, and os_optics.results reads all of it on your machine -- nothing is simulated again: every observation plane with its resolved frame (origin, u, v, normal, active area, absorbing behaviour), every arriving beam's full state (position, direction, power, optical path, phase, Jones vector and Stokes parameters, Gaussian q / waist / radius, first-order ray-transfer matrix and image orientation, lineage), the beam-path tree, the power budget, the chief and pupil rays at each plane, geometric wavefronts, irradiance maps and the meaning of every metric. Your own Agent can then plot, fit and report whatever it needs.

import tempfile
from pathlib import Path
from os_optics.local import example_project_of_kind, run_optical_project
from os_optics.results import load_run

run_dir = tempfile.mkdtemp()
run_optical_project(example_project_of_kind("two_beam_interference"), output_dir=run_dir)
results = load_run(run_dir)                                  # also: the run's {file name: bytes}, or the run itself
print(results.describe()["available"]["monitors"])           # the observation planes of this run
for arrival in results.monitor("crossing_plane")["arrivals"]:
    beam = arrival["beam"]
    print(beam["path_id"], beam["power_W"], beam["gaussian"]["beam_radius_x_mm"], arrival["polarization_state"]["s1"])
wave = results.wavefront("crossing_plane", "SRC#0")          # OPD per pupil ray (waves, piston removed), RMS, PV
print(wave["status"], wave["rms_waves"] < 1e-3)
Path("arrivals.csv").write_bytes(results.export("arrivals", "csv"))   # one row per arrival, units in the column names
budget = results.power_budget()                              # where the power went: input, sinks, residual
print(round(budget["total_input_W"], 6), sorted(budget["total_sinks"]))

From a shell:

os-optics results describe run                    # identity, status, available results, conventions, files
os-optics results monitor run focal_plane         # one plane: every arriving beam's state, coherent sums, targets
os-optics results wavefront run --monitor focal_plane --path 'SRC#0' --reference sphere --center ray_focus
os-optics results metrics --name waist_distance_x_mm   # unit, definition, sign convention, validity
os-optics export arrivals run --format csv --out arrivals.csv
os-optics export rays run --monitor focal_plane --format jsonl --out rays.jsonl

Export kinds: monitors, arrivals, rays, paths, segments, power-budget, superpositions, wavefront, irradiance, metrics, handoff, exposures, power-ratings, as CSV, JSON Lines or JSON (os-optics results columns KIND documents every column and its unit); the dense irradiance grid also as NPZ. The wavefront is geometric optics (optical path differences of the traced pupil rays, measured from the source's Gaussian phase front) and the irradiance map is the paraxial Gaussian superposition: neither includes diffraction. A wavefront the data cannot support -- after an ideal or tabulated thin lens, or with too few rays -- is refused with a reason code instead of a number. Each metric descriptor names the result field that carries its value (result_fields: divergence_half_angle_x_rad is arrivals.far_field_half_angle_x_rad).

For a tool that propagates fields itself, results.handoff(monitor, element=None) (os-optics results handoff RUN --monitor ID [--element ID]) gives the beams at a plane in its (u, v, n) frame -- complex q per axis, waist, curvature, M², Jones vector and basis, power, medium -- with the TEM00 field-construction convention, each beam's validity (an aperture upstream keeps the power exact but not the diffracted spot) and a named element's clear apertures in the same frame. A run exports beam parameters, not a sampled field: intensity alone does not determine a field, and M² > 1 alone does not determine a beam's modes.

A pupil mask, its focal field and a detector

Two calculations continue from the light a run delivers, or from a light you state, stage by stage. Each quantity says how it is known: MODELED, EXTERNALLY_CHARACTERIZED (a value you stated, with its source) or UNAVAILABLE (with a reason; what does not need it keeps its value).

os-optics results pupil-field --request request.json > field.json       # add RUN --monitor PUPIL for a run's beams
os-optics results detector --field field.json --detector camera.json    # or RUN --monitor ID for a plane's irradiance

pupil_field takes a mask -- a ramp, a binary grating, a sampled phase and amplitude, a file with its SHA-256, or a weighted Gerchberg-Saxton mask for a list of spots -- with its pixel pitch and fill, to the scalar focal field behind an ideal lens, with the power at every stage. It holds up to a marginal-ray sine of 0.2; above that the focal quantities are UNAVAILABLE and the pupil stage and its power ledger stay: a tight focus belongs to an external vector-diffraction tool. detector_response takes an irradiance map to photons, electrons, ADU, saturation, noise and SNR per pixel; every detector parameter is optional, and what is not stated leaves what needs it UNAVAILABLE. os-optics schema pupil-field-request and os-optics schema detector-model print the inputs; docs/PHYSICS_SCOPE.md (P10e) has the conventions, the validity limit and the closed forms they were validated against.

Spacing lenses and mirrors: paraxial analysis

paraxial_analysis evaluates placed components with their real prescriptions and material dispersion at a wavelength (the Engine's thick-lens matrices): per element and for the system the ABCD matrix, effective focal length, back and front focal distances and principal planes; solve: "afocal" gives the vertex gap that makes two elements afocal and where to move the second one, solve: "collimate" the distance from a point source or a source's waist to the front focal point (with each source axis's output waist, divergence and state: COLLIMATED, or FOCUSED when the source's Rayleigh range exceeds the focal length). For cylindrical lenses, axis: "X" or "Y" names one section of the whole system (the source's x / y, else the first component's local X / Y); a cylinder turned to any other angle is refused. The Project needs a light source only when the request names source_id.

Mirrors take part directly. A mirror of vertex radius R is ((1, 0), (-2 / R, 1)) (R > 0 is concave, focal length R / 2), a plane mirror the identity; a mirror reflects on the side its local +Z faces. A curved mirror lies at normal incidence, a plane mirror at any tilt folds the axis. Every distance is on the unfolded axis (*_along_axis_mm, vertex_gaps_mm, bfl_mm), every *_mm point on the real folded path, and axis_path lists its straight stretches with their directions and section axes. A two-mirror telescope with primary R = +800 mm and secondary R = -150 mm 350 mm apart reports EFL 1200 mm and BFL 150 mm, its rear focal point 150 mm from the secondary along the returning light. Without a source_id, a curved first mirror takes the light along its axis and a plane first mirror from the direction that reflects towards the next element.

A COMPOUND unit (an imported prescription, for example) is composed from its members' poses: the entry has the unit's matrix and its members. Lenses meet the light in pose order; a unit with a mirror takes its members in the order it declares them. A lens the light returns through after a mirror is listed again where it meets it (["L1", "M1", "L1"], a double pass); a list that leaves such a crossing out is refused with PARAXIAL_ELEMENT_CROSSED_AGAIN, which gives the list to send. Refusals name the actual reason: PARAXIAL_COMPOUND_MEMBER_NOT_COAXIAL (the member and its pose), PARAXIAL_REFLECTOR_BACK_FACE, PARAXIAL_CURVED_REFLECTOR_TILTED, PARAXIAL_REFLECTOR_BEHIND_SURFACES (a second-surface mirror), PARAXIAL_NOT_COAXIAL, PARAXIAL_ORDER_INVALID, and PARAXIAL_ELEMENT_UNSUPPORTED for a SURFACE_SET element or an axicon.

from os_optics.local import example_project_of_kind, paraxial_analysis

project = example_project_of_kind("beam_expander").payload()   # SRC -> L1 (f ~ 50 mm) -> L2 (f ~ 100 mm) -> output plane
analysis = paraxial_analysis(project, {"components": ["L1", "L2"], "wavelength_nm": 780, "solve": "afocal"})
solution = analysis["solution"]
print(round(solution["vertex_gap_mm"], 3), [round(v, 3) for v in solution["position_mm"]])   # where L2 goes: afocal pair
print(round(solution["beam_size_magnification"], 2))                                          # about -2: expanded, inverted

From a shell: os-optics project paraxial project.json --components L1 L2 --wavelength-nm 780 --solve afocal, or --components L1 --wavelength-nm 780 --solve collimate --source SRC, or --components M1 M2 --wavelength-nm 633 for a mirror pair. Run the Project afterwards for the traced beams.

External scientific tools

OpenSci works with optimizers, wave-optics and full-wave solvers, quantum-photonics simulators and CAD kernels you run yourself. os-optics interop list names each workflow with its status, how far it is checked: GUIDANCE_ONLY, DATA_CONTRACT_AVAILABLE, VERIFIED_EXAMPLE (an executed example with pinned versions), PLANNED or DEPRECATED. os-optics interop show ID (for example interop show quantum-photonics; Python: os_optics.interop.interop_catalog()) gives an entry in full: its candidate tools and official links, what OpenSci provides and what the tool computes, the OpenSci exports and commands to use, the inputs you provide, and the assumptions and limits. External results stay in their own files and never change a run or its Local Validation. os-optics interop list --status VERIFIED_EXAMPLE lists the entries with an executed example, and os-optics interop show ID names each of them: the tool and its version, the Python and Engine versions, the date, the example's path and the companion-repository commit it was run at, and its outcome against the independent reference.

Power on your optics. At a glance:

  • What the bounds count. Every beam the trace follows; beams of one source and line add their fields with open phases, so a surface's exposure is a range. With power ratings also the light the trace does not follow (ghost reflections, a coating's transmission, light passing an aperture or an element), followed to every surface, and the light left out as negligible or of unknown fate whose power is known: at the diffraction limit of its in-phase power, times its open paths' crossings and its returns between surfaces facing each other.
  • Closed returns. Left-out light that may circulate through three or more surfaces of separate elements (a ring, a folded or coupled cavity, a periscope's windows through its folds) leaves every surface it may reach without an upper bound: its rating comparison and power_rating_utilization target stay undecided, an OUT_OF_MODEL_SCOPE issue per surface names the return's surfaces (closed_return_surfaces), and the traced hits and incident power are still reported. A stack of parallel planes (windows, plates, flat mirrors on one axis) keeps a bound that counts all its returns where a series of them converges, and is treated like a ring where none does.
  • Not counted: closed paths inside one optical solid (a prism's or a cube's internal folds, total internal reflection in prisms, cubes and lens bodies) and light that refraction turns back onto a lens or a refracting surface it already passed, before it is reflected again: those surfaces keep the pair and open-path bounds. There is no thermal or damage model.
  • Coatings. A catalog or Project coating keeps its published values beyond its published angles and wavelengths (held flat out to 0 / 90° and 100 / 20000 nm), for traced and left-out light alike; a traced beam that meets it beyond its published data is listed in model_scope_issues (the run at least OUT_OF_MODEL_SCOPE, FAIL when a target also fails).
  • What decides. WITHIN_RATING when the exposure's upper bound is within a rating that applies as stated, or whose test beam is at least as large as the light, and within every rating that may apply; EXCEEDS_RATING when its lower bound is above a rating that applies as stated, or whose test beam is smaller than the light. A smaller-beam rating -- and a rating that cannot be checked, whose threshold may be at most its value -- leaves the surface no upper bound unless a rating of the same unit that applies as stated or with a larger test beam has a lowest damage threshold no larger than its highest (W/cm values compared as P/d of the 1/e² diameter).
  • Several elements. Ghosts whose Gaussian state is not valid and the light left out as negligible count at their diffraction limit, so the exposure's upper bound lies far above the traced beams' and a utilization target there passes only with a large rating margin. An exceedance in W/cm² or J/cm² is decided at a much smaller margin, since that light, of small power, cannot undo a traced beam over the area it covers; in W/cm the margin is larger, since a thin sheet of that light meeting a face at a shallow angle keeps its P/d along a strip of the face. Coatings of low reflectance on the faces (declared or from the catalog) and fewer surfaces that may reflect the light make the bound smaller; where that light alone keeps a rating undecided, the run follows it deeper by itself.

results.exposures() (os-optics results exposures run [--element ID]) lists every element surface the beams meet: incident power, both Gaussian beam radii, angle of incidence, footprint, peak irradiance 2P/(π w_x w_y), P/d and, for a pulsed source (source_model.pulse: repetition rate and FWHM duration; powers stay average powers; pulse energy P / f_rep), the peak fluence. The pulse train changes nothing else: the Engine does not broaden a pulse by dispersion or separate its lines in time (group-velocity walk-off), reads coatings at the line's wavelength and does not model a mirror's group-delay dispersion. A peak fluence is the energy of one pulse per area. A pulse holds every line of its source along one path; the pulses of different sources, or of one source along different paths, need not arrive together: they are bounded from below by the strongest single pulse and from above by their sum. Light of different lines or different sources adds; beams of one source and line are mutually coherent and add their fields -- also when they cross the surface in opposite directions (a beam and its own reflection returning: a standing wave) --, so two equal branches of an interferometer reach four times one branch in phase and nothing in antiphase. With the phases open, such a surface's exposure is a range (coherent_groups, reason COHERENT_BEAMS_BOUNDED): at most Σv + 2Σγ√(v_i v_j) (the in-phase maximum (Σ√v)² when the source declares no coherence length; otherwise the degree of coherence of the Gaussian spectrum of L_c after the two beams' relative group delay τ -- the difference of their group optical paths Σ n_g L, not of their phase paths -- and group-delay dispersion, at its largest over the surface: collinear beams keep about their chief rays' value, beams crossing at an angle are fully coherent somewhere in their overlap, and a beam that crossed glass other than a plate -- a lens, a prism, a cube -- is fully coherent with its line, its envelope delay across it not being followed, and so are beams that cross passes of undetermined group delay differently; a pulsed source's pulses may be mutually coherent, so two of its beams count as coherent again near every multiple of its period length c / f_rep, where the coherence metrics then have no value), at least max(0, √v_max − Σ√v_others)² at a traced beam's centre -- and at least that beam's mean over a window on its area less what light of unknown place (a ghost of unknown size, the light left out as negligible) can take away there with its small power; in P/d that light counts as beams crossing the face from one side, each over the strip of the window it covers. The plane analyses report the same coherence (group_path_difference_mm, coherence_factor; arrivals[].group_path_length_mm). Light weaker than energy_conservation_tol times its line's input is not followed and paths stop at the power floor; that light still goes somewhere, and focused it can matter however faint. It is left out in pieces of one line, mutually coherent: where they meet their fields add, so the bounds use their power together in phase, P = (Σ√p)² (exposures.faint_light[].in_phase_power_W). With power ratings that P per line -- with the light whose fate the trace does not follow but whose power it knows: its budget, a path ended out of model scope (exposures.faint_light) -- bounds every surface by its diffraction limit 2πn²P/λ² (the most light of power P crossing a surface from one side can concentrate; n the larger index of the surface's media; for a broad spectrum the mean of (λ/λ')² over it, 1.0025 for a 10 fs pulse at 800 nm), times the most its open paths' crossings bring onto a surface or across a plane, √2 m Σ_{j=0..N} a_j, at least 2 (between two reflections the light meets each lens and each refracting surface between media of different index at most once and travels in at most L = T + 1 straight legs, T those lenses and refracting surfaces, each meeting any other surface as often as a straight line can, K times: a plane once, a sphere or a conic twice, an asphere by the degree of its sag; so it meets a lens or a refracting surface between different media at most m = 1 time and any other surface at most m = K L times; after j reflections it is made of pieces from different sequences of j of the N surfaces that may reflect it, mutually coherent, whose fields add: a_j is the smaller of the sum over those sequences of the products of m_s √R_s and the square root of their number times the power they share, R_s a surface's reflectance bound -- 2.26 on the faces of four lenses with 0.25 % coatings, 5.41 on an uncoated window's faces; a transparent plane's power gains at most m Σ_{j=0..N} min(1, R^j), at least 2, times the pieces' power, R the sum of the reflectance bounds, each times its m), and more where two reflecting surfaces facing each other return it again and again -- a window's, a lens's or a cube's faces, a window and a mirror, the mirrors of a linear cavity, also with lenses or plates between them: each round trip keeps at most g = r_a r_b of its field (the two amplitude reflectances over every angle at which the pair's light can meet them, below the critical angle), so all its returns bring at most g/(1 − g) times one crossing more, and what each return sends out of the pair -- through its surfaces, past an aperture's edge, reflected by a surface between them -- is at most that return's field and may reach any surface: every surface and plane, from either side, gets f = 1 + Σ g/(1 − g) over the scene's pairs (faint_light_return_factor; about 1.04 in a scene whose only pair is an uncoated window; FAINT_LIGHT_RECIRCULATES): it adds 2A Σ√v + A² to the upper bound and takes A off the amplitudes of the bound at a beam's centre (FAINT_LIGHT_BOUNDED, faint_light_peak_W_per_cm2); a rated surface no other light reaches lies between 0 and A². The Engine computes with its coating model: a catalog or Project coating compiles to one that holds its published values beyond its published angles and wavelengths, for traced and left-out light alike. A pair whose gain bound is 1 (two surfaces that reflect everything, a coating met outside its model's data -- a declared coating with a stated angle limit --, the faces of an uncoated lens, which light inside it can meet up to the critical angle) and light at the per-path interaction limit (light that may be stored in a cavity the traced light shows) bound nothing: no upper bound on any surface. Closed returns through three or more surfaces of separate elements (a ring, a folded or coupled cavity, a periscope's windows through its folds) withhold the upper bound of every surface they may reach, with an OUT_OF_MODEL_SCOPE issue per surface naming the return (closed_return_surfaces); a stack of parallel planes keeps an all-orders series of its returns where one converges (1/(1 − A) with A = Σ m_s r_s < 1, else the alternating series of its reflections along the planes' normal) and is withheld the same way where none does (reference: rings, folded and coupled cavities, plate stacks, tests/test_closed_returns.py, tests/test_closed_return_stacks.py). Not counted: closed paths inside one optical solid (a prism's or a cube's internal folds, total internal reflection in prisms, cubes and lens bodies); light that refraction turns back onto a lens or a refracting surface it already passed, before it is reflected again. Between two parallel planes a refracting plane parallel to them keeps the light's momentum and counts in its walk; a tilted plate or a wedge beside them, which shifts the light sideways, is screened as a surface that turns it. Where that light alone keeps a comparison from being decided, the run follows the line's light deeper (down to the power floor at most) so that the probes' known spots replace the bound; the probes carry the pupil rays of two whole rings of the beam they leave, so an aberrated ghost or leak counts with the diffraction limit of its power, never as a paraxial spot. Each surface is compared with its element's power ratings -- a catalog product's published power_ratings or the power_ratings you declare for your own model -- condition by condition (ratings[].conditions: VERIFIED, DIFFERS with its effect, NOT_VERIFIABLE with its reason, or NOT_STATED for a condition that does not apply to the rating, such as a pulse count to a CW rating). A test condition the rating does not state -- its test beam diameter, a pulsed rating's repetition rate or pulse count, its polarization, its value's definition -- is not verifiable (RATING_INFORMATION_MISSING): the threshold depends on it.

  • regime, wavelength within 2 %, pulse duration (a rated duration read as a FWHM) and repetition rate within 10 % -- otherwise the rating does not describe this light (no scaling between wavelengths, durations or rates); followed light of a line the rating is not for that carries at most 1 % of the most light the surface can receive, in peak irradiance and every quantity that gives a comparison its direction, counts in the exposure but not in these conditions (a note names it); light left out as negligible of a line the rating is not for leaves each condition that line differs in not verifiable once it may carry more than 1 % of the most light on the surface (in peak irradiance or in the rating's own quantity, whichever share is larger); a surface only that light reaches is compared with its lines' regime, wavelength and pulse train, and where light may be stored between surfaces facing each other (nothing bounds it) a line the rating is not for leaves its conditions not verifiable whatever its power;
  • the test beam on the sample (beam_diameter_mm and its definition, a diameter without its definition read as the 1/e² to the FWHM width) against the 1/e² size of every beam on the surface: within 5 % of a stated 1/E2 or FWHM diameter it is verified; a beam no larger than the test beam (its footprint on the surface included) has a damage threshold at least the rated value; a beam narrower nowhere than the test beam has a threshold at most the rated value (small test spots overstate the threshold of larger beams). A diameter whose definition the rating does not state is read as a 1/e² diameter from the stated value to 1.70 times it (FWHM, 1/e, second-moment and 1/e² readings of a near-Gaussian beam): a beam between those readings, or one a reading would verify, is not verifiable. On a metallic reflector a pulsed rating keeps no direction across beam sizes unless it is a single-pulse rating applied to one pulse per site; a W/cm rating keeps none for a larger beam. Light whose size is not known (light left out as negligible, untraced light bounded by its diffraction limit) and the weakest beams that carry, with the followed light of unknown size, at most 1 % of the most light the surface can receive in every quantity that gives the size its direction (the rating's own, and for a CW rating the power and P/d, for a pulsed one the peak fluence and the pulse energy: a faint ghost of another size; a beam whose chief ray misses the surface counts with its tail there) do not decide the size; beyond that share such light keeps a smaller-beam or matching rating from applying (BEAM_SIZE_UNKNOWN);
  • the pulse count against the pulses one site of the surface receives: every crossing of a source's light there brings that source's pulse.total_pulses (how many pulses it sends while the optics stay in place: a whole number, or UNLIMITED for a source that keeps pulsing as long as they are in use; the lines of one source along one path are one crossing); crossings that together carry at most 1 % of the most light on the surface do not count. A rating's count is compared per tested site where that is what it means -- a single-pulse rating, or a count the rating states as applied at one fluence level to each tested site (test_protocol S_ON_1) --, and damage is irreversible: every site receiving the test's count is verified, more pulses give a threshold at most the rated value (only an exceedance is decided), fewer a threshold at least the rated value (only "within"); crossings that may bring a site fewer or more are PULSES_PER_SITE_UNDETERMINED; a source that states no total_pulses leaves it not verifiable (DESIGN_INPUT_MISSING). A rating that states no count, or a count above 1 without saying how it was applied -- every multi-pulse catalog rating --, is not verifiable whatever the totals (RATING_INFORMATION_MISSING); continuous light differs, as its regime does;
  • the test polarization (s-polarized / s / TE, p-polarized / p / TM, linear, circular, unpolarized: every beam within 1 % of one common state, so that any mixture of them holds it -- beams of another state that carry at most 1 % of the most light count in the exposure, not in the state; a rating whose text names another state than its machine-readable polarization is refused; one stated in other or more words, s or p at normal incidence, beams of different states whose mixture is not decided, or light left out as negligible, of a state the trace does not know, that may carry more than 1 % of the most light (in peak irradiance or in the rating's own quantity, whichever share is larger) is not verifiable).

The rated value is a damage threshold for the peak exposure the Engine computes as its definition establishes one: a J/cm2 or W/cm2 value stated as a peak value (value_definition PEAK) is the threshold; one stated as the total energy (or power) over the area of the test beam's 1/e² diameter (TOTAL_PER_1E2_AREA) is half a Gaussian beam's peak, so the threshold is twice it (ratings[].rated_threshold_low / _high). A value whose definition is not stated -- every catalog rating -- establishes no threshold: its value_definition condition is not verifiable and the comparison is CONDITIONS_NOT_VERIFIED, with the exposure reported. A W/cm value (P/d) is its own threshold. A TYPICAL value bounds the threshold of no particular part. Fluence and irradiance are compared in the beam's transverse plane (on a 45° face the fluence on the surface is cos 45° of it); no rating states the angle of incidence or the plane of its test. The rating's applicability follows: AS_RATED (every stated condition verified: WITHIN_RATING when the exposure's upper bound is at most rated_threshold_low, EXCEEDS_RATING when its lower bound is above rated_threshold_high, else UNKNOWN), THRESHOLD_AT_LEAST_RATED (only WITHIN_RATING is decided), THRESHOLD_AT_MOST_RATED (only EXCEEDS_RATING), a TYPICAL value (neither), NOT_VERIFIED (CONDITIONS_NOT_VERIFIED, the exposure -- exact, or its bounds -- still reported) or NOT_APPLICABLE (a condition differs with no established relationship, or two in opposite directions: CONDITIONS_MISMATCH, never an exceedance). A W/cm rating that does not state its diameter definition is compared with P/d from the 1/e² to the FWHM diameter. An exceedance raises the notice POWER_RATING_EXCEEDED, which does not change the run's status; a surface is UNKNOWN when the range straddles the rating (no valid Gaussian beam there, coherent beams with open phases, an incomplete trace, or light the trace does not follow -- ghost reflections, a mirror coating's transmission -- that may reach it: with ratings that light is followed to the elements), and RATING_MISSING without a rating. A target on power_rating_utilization (device_instance_id = the element) makes the rating a requirement: the element's utilization (exposure / damage threshold, from rated_threshold_high below to rated_threshold_low above) lies between the largest lower bound of the ratings that apply as stated or with a threshold at most rated and the largest upper bound of those that apply as stated or with a threshold at least rated and of every rating that may apply (none while a rated surface has none: a rating with a threshold at most rated, or one not verified, leaves none unless a rating of the same unit that applies as stated or with a threshold at least rated has a lowest threshold no larger than its highest, W/cm values compared as P/d of the 1/e² diameter; where a rating with a threshold at most rated has only larger ones the ratings disagree and the lower bound stands); the target PASSes when that interval is within the limit and FAILs when its lower end is not, its value is the largest exposure / damage threshold of the ratings that apply (the exposure exact, or its lower bound; no value when none of those exposures is known or bounded from below), and its message names what makes the utilization an interval. For a pulsed source the peak irradiance is the spatial peak of the average irradiance; the pulse is described by its energy and peak fluence. There is no thermal or damage model.

Two-colour alignment. centroid_separation_mm, normalized_centroid_separation, angular_separation_rad, geometric_overlap_ratio and waist_position_separation_mm compare two beams on a plane -- two lines of one source (wavelength_nm, wavelength_nm_b) or two sources (source_id, source_id_b) -- by their chief rays and Gaussian footprints. They are geometric co-alignment measures, not interference: beams of different wavelengths are compared directly.

The catalog and the model basis

os-optics catalog list / load_public_catalog() give the active release of the OpenSci optical catalog: lenses (plano-convex / -concave, bi-convex / -concave, positive / negative meniscus, best-form, aspheric, cylindrical, cemented achromatic doublets), flat, concave and off-axis parabolic mirrors, plate and cube beamsplitters, polarizing cube beamsplitters, beam samplers, dichroic mirrors, long-pass / short-pass / band-pass / notch / neutral-density filters, right-angle and equilateral prisms, windows, half- and quarter-wave plates and linear polarizers. Every product has its specifications, published simulation model and design geometry, an English name (display_name) and a Simplified Chinese name (display_name_zh_cn); optical_sku (OS-OPT-...) is the product identity. On Windows, set PYTHONIOENCODING=utf-8 to see the Chinese names directly in catalog list output; otherwise they are printed as JSON \u escapes.

Orderable, mounted, housed. Every product states its commercial_readiness: ORDERABLE (OpenSci supplies it) or SIMULATION_ONLY (for design and simulation; in an ordered design you supply it yourself, and a design made only of simulation-only products cannot be ordered). Prefer ORDERABLE products when the design is to be bought from OpenSci. An ORDERABLE product also states opensci_mounting: AVAILABLE -- bought with OpenSci mechanics (a complete system, mounts per optic, or a part list with derived mechanics) it comes with an OpenSci mount, post and post holder; or NOT_AVAILABLE -- OpenSci has no mount for it, so it is supplied without mounting hardware: bought as optics only or on a part list it ships like any optic; in a design bought with OpenSci mechanics it ships unmounted and you mount it at its design position (the layout keeps its space and beam path clear, and the OpenSci assembly service is not offered for that design); a part list with derived mechanics cannot derive its mount (MOUNTING_NOT_AVAILABLE). delivered_form HOUSED means the delivered part is the optic in its manufacturer's housing: the optics are those of the element, the outline is the housing's, so allow for its size. os-optics catalog list shows the readiness in each row and, for an ORDERABLE product, its OpenSci mounting (--readiness ORDERABLE, --mounting AVAILABLE filter by them).

Finding a lens by focal length. The focal filters compare the signed nominal focal length a product publishes (focal_length_mm, at its design_wavelength_nm when it states one), in millimetres. Negative lenses lie below zero, so bounds are signed. Compose them with every other filter and with paging; total counts the matches.

os-optics catalog list --category LENS --focal-length-mm -50                           # exactly -50 mm
os-optics catalog list --category LENS --focal-length-mm -50 --focal-tolerance-mm 5    # -55 mm to -45 mm
os-optics catalog list --category LENS --focal-min-mm 140 --focal-max-mm 160           # 140 mm to 160 mm, inclusive
os-optics catalog list --category LENS --focal-sign negative --limit 20                # any negative lens

The same in Python: catalog.select(category="LENS", focal_min_mm="-60", focal_max_mm="-40", focal_sign="negative"), and in the JSON API: catalog_products(focal_length_mm=..., focal_tolerance_mm=..., focal_min_mm=..., focal_max_mm=..., focal_sign=...). --focal-tolerance-mm is your own search tolerance, separate from the focal_length_tolerance_percent a product publishes. A product that publishes no focal length (a flat mirror, an axicon) never matches. A filtered response carries focal_filter: your query, the bounds compared and excluded_without_focal_length, how many otherwise matching products publish none. A malformed or self-contradictory query is refused with a reason (FOCAL_VALUE_INVALID, FOCAL_BOUNDS_CONFLICT, FOCAL_TOLERANCE_WITHOUT_VALUE, FOCAL_TOLERANCE_NEGATIVE, FOCAL_RANGE_INVERTED, FOCAL_SIGN_UNKNOWN, FOCAL_SIGN_CONTRADICTS_RANGE): on stderr with exit code 64 from the command line, in details.reason of CATALOG_QUERY_INVALID from the JSON API. --wavelength-nm keeps its meaning (a simulation model valid at that wavelength) and does not change the value compared; project paraxial gives a placed component's focal distances at a chosen wavelength.

Model basis.

  • Surfaces are planes, spheres and sag surfaces (conic, even asphere, cylinder, off-axis sections). Lenses, windows and refracting prisms are traced surface by surface through their prescription with the dispersion of their glass (SURFACE_PRESCRIPTION); mirrors, beamsplitters, beam samplers, prisms with a coated face and some band-pass filters and dichroic mirrors answer with their specified coating envelope (SPECIFICATION_ENVELOPE); waveplates, polarizers and the other filters and dichroic mirrors are ideal thin elements (IDEAL_THIN_ELEMENT, Jones matrices). Each product's fidelity names its basis. Cubes, prisms, wedged plates and off-axis mirrors are traced as a set of surfaces in 3D (SURFACE_SET).
  • Rays follow each beam's chief ray plus deterministic pupil samples, with one optional Fresnel reflection per interface.
  • Gaussian beams are paraxial (x/y-independent q-parameters); polarization is fully polarized Jones calculus; interference is an analytic two-beam model; materials are evaluated at their catalogue temperature.
  • Wave-optics quantities as run targets (propagation through apertures and pinholes, PSF, MTF, far-field patterns), stray light and scattering are reported OUT_OF_MODEL_SCOPE. The scalar low-NA focal field of a pupil is os-optics results pupil-field.

Your own optics: declared optical models

A Project can declare optical models by their physics, without a catalog product: a lens of your own prescription, a multi-element objective, a pinhole, a slit, an ideal lens. A declared model is compiled by the same rules into the same Engine surfaces as a catalog product, traced by the same Engine, checked against its own band and coatings, and drawn in the 3D view as its real body. A Project made only of declared models needs no catalog.

  • optical_models: each model has a model_id and a model:
    • SURFACE_SEQUENCE: coaxial surfaces along local +Z (plane, sphere, conic, even asphere, cylinder and acylinder, the axicon cone term linear_sag_coefficient, and the two-variable sag shapes sag: Zernike, XY polynomial, biconic, toroid; circular or rectangular, optionally decentred clear apertures) and the media between them. AIR is a valid gap medium, so an air-spaced objective of up to 16 surfaces is one model. Each surface may state its own mechanical edge (edge_radius_mm on the surface; the model's edge_radius_mm is the largest one): the surface reaches its edge and is a flat flange beyond it, so the lenses of an air-spaced group keep their own diameters and edge thicknesses (the trace blocks light on that flange zone at the surface's continued profile: KNOWN_LIMITATIONS.md). Behind an axicon the light forms a Bessel-like zone: rays and powers are traced, Gaussian beam metrics are out of model scope (GAUSSIAN_UNDEFINED_AFTER_AXICON);
    • SURFACE_SET: surfaces posed anywhere in the element frame, optionally inside a convex solid (cubes, prisms, off-axis mirror sections);
    • POLARIZATION_ELEMENT: a retarder or linear polarizer (Jones matrix) with its axis on local +X;
    • APERTURE_STOP: an opaque plate with a circular opening (iris, pinhole), optionally with a central obstruction (an annulus), or a rectangular opening (a slit), decentred if you like. Light inside the opening passes, light on the plate is blocked, light beyond the plate's outline passes by. It is a geometric stop: diffraction at the opening is not modelled;
    • IDEAL_THIN_LENS: a perfect lens of a given focal length at one plane (no thickness, aberration or optical-path phase);
    • TABULATED_THIN_LENS: a thin focusing element whose response comes from a table -- per wavelength its focal length and the fraction of power delivered into the focused beam -- for example a metalens response computed with an external RCWA or wave-optics tool. Each source line is focused with its own focal length; the undelivered power is booked UNMODELLED_RESPONSE. The model states where it holds (max_field_angle_deg, optional max_beam_radius_mm and validated_polarizations), which result metrics it was validated for (supported_metrics, each with its error; transmittance covers the power metrics) and its evidence (method, tools and versions, source-data SHA-256, fit, validation summary, date). Light outside that domain is an Engine issue of status OUT_OF_MODEL_SCOPE (MODEL_WAVELENGTH_OUTSIDE_BAND, MODEL_FIELD_ANGLE_EXCEEDED, MODEL_BEAM_SIZE_EXCEEDED, MODEL_POLARIZATION_NOT_VALIDATED; model_scope_issues also lists each with the observations and targets that light reaches, whose assessment is then INDETERMINATE), and a target on a metric the model was not validated for is OUT_OF_MODEL_SCOPE (METRIC_NOT_VALIDATED_FOR_ELEMENT); the run is then at least OUT_OF_MODEL_SCOPE, FAIL when a target also fails. The run summary carries the model's domain and evidence. It has no optical-path phase, so wavefronts and network exports through it are refused;
    • DIFFRACTION_GRATING: a thin periodic plane, REFLECTION or TRANSMISSION, stated by period_nm or line_density_per_mm, the grating vector's angle, orders or all_propagating_orders, its media and aperture. Every order's direction follows from the vector grating equation, at any wavelength and angle of incidence (docs/PHYSICS_SCOPE.md, section P10d). Its response gives the order powers, with their normalization, polarization basis, domain, interpolation and provenance; without one, or outside it, an order's power_W is null with power_upper_bound_W, and its position and direction stay valid. Phase is not stated, so wavefronts and network exports through it are refused;
    • FARADAY_ROTATOR: a nonreciprocal polarization rotator stated by its response: rows of wavelength, rotation_deg and transmittance, with valid_aoi_deg, operating_conditions and evidence. Local +Z is the field axis: the field turns right-handed about it whichever way the light travels, so a round trip adds the rotation. Light of a wavelength the table does not cover stops at the element (MODEL_WAVELENGTH_OUTSIDE_BAND, status OUT_OF_MODEL_SCOPE). It has no optical-path phase, so wavefronts and network exports through it are refused. A polarizer, a rotator and an analyzer make an isolator: launch one source from each end and read each on its own (docs/PHYSICS_SCOPE.md, section P10c);
    • NEUTRAL_DENSITY: an attenuator plane whose optical density may vary across it: FIXED, a continuously variable wheel (LINEAR_ROTARY), a stepped wheel (STEPPED_ROTARY) or a linear gradient (LINEAR_GRADIENT), with an optional uncoated transmittance and reflectance. The density is evaluated where the beam crosses the plane; the component's selected_parameters entry named by position_parameter (nd_rotation_deg or nd_offset_mm) turns the wheel or shifts the gradient. A beam whose density varies by more than 0.2 OD across its footprint is an Engine issue of status OUT_OF_MODEL_SCOPE (ND_DENSITY_VARIES_ACROSS_BEAM);
    • COMPOUND: one optical unit (an objective, a relay, a telescope) made of member models at fixed poses in the unit's frame, with the unit's envelope and optional ports. The unit is placed as a whole; every member surface is traced. Member surfaces are named <member_id>-<surface_id> in the Engine (L1-S1, STOP-stop).
  • power_ratings (optional, on any model but a COMPOUND unit, whose members carry their own): your element's power-handling ratings with their conditions (regime CW / PULSED, value, unit W/cm / W/cm2 / J/cm2, wavelength_nm, pulse duration and repetition rate, test beam diameter, pulse count, test_protocol (S_ON_1 when each tested site received the count at one fluence level), value_definition (PEAK or TOTAL_PER_1E2_AREA for a J/cm2 / W/cm2 value, when your test report states it), polarization_condition, status DECLARED, basis), compared with the run's per-surface exposure condition by condition (above). The test beam diameter is the beam on the sample. A model whose reflecting surface carries a metallic-reflector coating counts as a metallic reflector; a reflecting neutral-density element or an aperture stop is of unknown kind, so a pulsed rating with a test beam there is not verifiable where a metallic surface would change the result (RATING_INFORMATION_MISSING); the polarization is checked when written as s-polarized, p-polarized, linear, circular or unpolarized.
  • model_materials: the materials of your models (Sellmeier, tabulated or constant index, optional bulk absorption_coefficient_per_mm); model_coatings: their coatings (the catalog's coating format). A declared model only uses these; in the Engine its materials are project:<MATERIAL_ID>, separate from catalog:<MATERIAL_ID>. A coating is piecewise-constant bands, a tabulated spectrum (unpolarized, or with reflectance_s / _p and transmittance_s / _p per wavelength) or an angular_spectrum: s / p tables over wavelength × angle of incidence, read at the angle in the medium the light arrives from and interpolated bilinearly. An optional phase states the coating's reflection and / or transmission phases per polarization (constant, or tabulated over wavelength), with r = √R e^{iφ}, t = √T e^{iφ} in the Engine's s / p triad and the time dependence exp(−iωt): a mirror, splitter or coated refracting face then uses them instead of its default phases, and the linear optical network export carries them. A coating keeps its published values (and its stated phases) beyond its published angles and wavelengths, for traced and left-out light alike; a traced beam that meets it beyond its published bands, spectrum or angles is listed in model_scope_issues (COATING_WAVELENGTH_OUTSIDE_BANDS, COATING_AOI_OUTSIDE_RANGE) and makes the Local Validation at least OUT_OF_MODEL_SCOPE.
  • A POLARIZATION_ELEMENT may resolve its transmittance (and a polarizer's extinction ratio) by wavelength with transmittance_bands: each spectral line uses its own band.
  • A source's polarization is linear ({"kind": "LINEAR", "angle_deg": ...}) or elliptical ({"kind": "ELLIPTICAL", "orientation_deg": ..., "ellipticity_deg": ...}; −45° is right and +45° left circular, the handedness seen by an observer looking toward the source, the same sign as the results' polarization_ellipticity_deg). Both angles are measured in the source's own frame, from its local +X toward its local +Y (the beam travels along local +Z), so the source's nominal_pose turns them: with the examples' pose quaternion_wxyz = [0.5, 0.5, 0.5, 0.5] (beam along design +X) local +X is design +Y and local +Y is design +Z, so angle_deg: 0 is horizontal (design Y) and 90 vertical (design Z).
  • A component uses a model through a custom_part and optical_model_id. Its optical_role must fit the model: an aperture stop is an aperture, a thin lens a lens, a retarder a waveplate, a polarizer a polarizer, an attenuator a filter, a Faraday rotator or a diffraction grating other_optical; surface models and compound units may play any optical role; other_optical fits every model.
  • frame and design_geometry are optional. Without a frame, a transmissive element has its first surface vertex at the origin and a reflector or plate splitter its coated face (the last surface, at z = 0); an element placed by its ports states its frame. Without a design geometry, the body is the outline between the surfaces' extreme z (NOMINAL); thin elements (stops, thin lenses, polarization elements, a single plane) have zero thickness.
  • design_intent: LAB_ASSEMBLY (the meaning when absent: a layout built from individually mounted components) or OPTICAL_SYSTEM (an optical design simulated, inspected and exported without laboratory mechanics). The run summary records it; physics does not depend on it.

Start from a catalog product with os-optics project model-from-catalog OS-OPT-... --model-id my-lens (or declared_copy(product, catalog) in Python): it prints the product's model, materials and coatings ready to paste into your Project, and it traces exactly like the product until you edit it.

from os_optics.local import run_optical_project

band = {"min_nm": "400", "max_nm": "1100", "status": "SPECIFIED"}
glass = {"material_id": "N-BK7", "display_name": "N-BK7", "valid_band_nm": band,
         "dispersion": {"kind": "SELLMEIER", "b": ["1.03961212", "0.231792344", "1.01046945"],
                        "c_um2": ["0.00600069867", "0.0200179144", "103.560653"]}}
lens = {"model_id": "my-lens", "model": {
    "model_kind": "SURFACE_SEQUENCE", "model_version": "2", "fidelity": "SURFACE_PRESCRIPTION", "status": "SPECIFIED",
    "surfaces": [{"surface_id": "S1", "vertex_z_mm": "0", "radius_mm": "51.5", "clear_radius_mm": "11", "traced": "TRANSMITTED", "status": "SPECIFIED"},
                 {"surface_id": "S2", "vertex_z_mm": "3.6", "clear_radius_mm": "11", "traced": "TRANSMITTED", "status": "SPECIFIED"}],
    "media": ["N-BK7"], "edge_radius_mm": "12.7", "valid_band_nm": band, "approximations": ["NOMINAL_PRESCRIPTION"]}}
pinhole = {"model_id": "pinhole", "model": {"model_kind": "APERTURE_STOP", "model_version": "1", "status": "SPECIFIED",
                                            "opening_radius_mm": "0.1", "edge_radius_mm": "12.7"}}
along_x = [0.5, 0.5, 0.5, 0.5]  # local +Z along design +X
project = {
    "schema": "opensci.project", "project_schema_version": "2.0", "design_intent": "OPTICAL_SYSTEM",
    "optical_models": [lens, pinhole], "model_materials": [glass],
    "components": [
        {"instance_id": "SRC", "optical_role": "source", "custom_part": {"custom_part_key": "laser", "description": "My laser."},
         "nominal_pose": {"position_mm": [-200, 0, 0], "quaternion_wxyz": along_x},
         "source_model": {"model_kind": "GAUSSIAN_BEAM", "wavelength_nm": 632.8, "power_w": 0.001, "waist_radius_mm": 1.0,
                          "waist_position_mm": 0.0, "m2": 1.0, "polarization": {"kind": "LINEAR", "angle_deg": 0}}},
        {"instance_id": "L1", "optical_role": "lens", "custom_part": {"custom_part_key": "my-lens", "description": "My lens."},
         "optical_model_id": "my-lens", "nominal_pose": {"position_mm": [0, 0, 0], "quaternion_wxyz": along_x}},
        {"instance_id": "PH", "optical_role": "aperture", "custom_part": {"custom_part_key": "pinhole", "description": "My pinhole."},
         "optical_model_id": "pinhole", "nominal_pose": {"position_mm": [101.2, 0, 0], "quaternion_wxyz": along_x}},
    ],
    "observations": [{"observation_id": "after", "pose": {"position_mm": [150, 0, 0], "quaternion_wxyz": along_x}}],
}
run = run_optical_project(project)  # no catalog needed: every optic is declared
print(run.status.value, run.catalog_version, [e["optical_model_id"] for e in run.summary()["element_models"]])

Import, build, inspect and export

  • Import a lens prescription. os-optics project import-prescription design.csv --model-id objective --into project.json --instance-id OBJ --out project.json reads a csv-surface-table/1 surface table (radius, thickness, glass, semi-diameters, stop, conic and aspheric terms; mm, cm, m or inch) and adds one COMPOUND unit with an element per glass group and the stop as an APERTURE_STOP member. Glasses the file does not define are copied from the catalog. Each problem of the first failing step (table, then glasses and coatings, then geometry) is reported with its row and column; nothing is imported from a file with problems. The format (sections, columns, directives): help(os_optics.prescription.csv_table).
  • Build a supplier-independent component. os-optics native list lists the component classes (stops, slits, annular apertures, central obstructions, ideal thin lenses, singlets designed for a focal length, cemented achromats, axicons, wedge prisms, windows, layered plates (a cuvette), mirrors, hollow roof mirrors and corner-cube retroreflectors, splitters, polarizers, waveplates, Faraday rotators of a stated response, diffraction gratings, variable neutral-density filters, lenses of a tabulated response, telescopes and a 4f relay); os-optics native describe CLASS gives its parameters and os-optics native build CLASS --param focal_length_mm=100 ... --into project.json --instance-id L1 adds it to a Project.
  • Check the optical bodies. Every run checks each optical body before tracing: a profile that ends inside its outline, glass thinner than zero anywhere across the outline, intersecting surfaces, an inconsistent medium chain, overlapping bodies of different elements and members outside their unit's envelope are refused with the element, member and surface named. os-optics project solids project.json prints the full report, knife edges and touching bodies included. These are checks of the optics themselves; no mount, post or breadboard is needed.
  • Look inside. os-optics view run --section ALL --snapshot shots draws the 2D layout of a lens: one exact true-scale section through every element on the axis, with each surface's profile, the glass between them, clear-aperture marks, the stop, the image plane and the run's rays, framed on the optics. --section OBJ does the same referred to one element or unit (repeat the flag or separate ids by commas), and --frame scene or --frame=X0,Y0,Z0,X1,Y1,Z1 changes what it shows. The 3D view lists every surface of the selected element to show, hide or click.
  • Export a linear optical network. os-optics project network project.json --out network.json writes the design's optics as an opensci-linear-optical-network/v1 document: input modes (each source in two polarizations), output modes on the observation planes, the complex transfer matrix with its conventions, the path terms behind it and a check against the Engine's own coherent result, for use in your own analysis or quantum-optics tools. Optics that are not a linear network (a cavity, mixed wavelengths, partly overlapping beams, an ideal or tabulated thin lens on an interfering path, ...) are refused with every reason. Each document states its conventions (conventions); os-optics schema linear-optical-network prints the format.

The same functions are in os_optics.local.json_api (import_prescription, native_components, build_native_model, catalog_model, inspect_optical_solids, optical_section, linear_optical_network) and in the browser Designer.

os_optics.local.json_api (opensci.local_json_api 1.5) answers every call with JSON-safe data. json_api.capabilities()["json_api_methods"] lists its methods, and json_api.call_method(method, params, file_map=..., data=...) calls one by name with its parameters as a JSON object (the files or bytes a method takes go in file_map / data; the reply's files come back beside it): the entry point for a host that is not a Python program, such as the browser Designer's Engine worker. An unknown method answers METHOD_NOT_IN_RUNTIME, parameters that do not fit it INPUT_INVALID.

Compose systems from reusable layouts

Design a large system as modules: author and validate each stage as its own Project, export it as a module, import it into the system as many times as you need, arrange the instances and validate the whole system.

os-optics workspace init fold.json --module fold.descriptor.json -o fold.ws.json   # mark test fixtures, interfaces
os-optics workspace run fold.ws.json                                               # the module on its own
os-optics workspace export fold.ws.json --descriptor fold.descriptor.json -o fold.module.json
os-optics workspace init system.json -o system.ws.json
os-optics workspace import system.ws.json fold.module.json --key a --rebind SRC=LASER --op-id import-a
os-optics workspace import system.ws.json fold.module.json --key b --rebind SRC=LASER --position 0,100,0 --op-id import-b
os-optics workspace transform system.ws.json --instance b --axis 0,0,1 --angle 90 --pivot component:b_M1 --op-id turn-b
os-optics workspace run system.ws.json --out-dir run
os-optics workspace status system.ws.json      # which validations are current, instances, groups
  • Each import is an independent instance: ids become <key>_<id>, also those of the models, materials and coatings it copies (every instance owns its definitions, so editing one instance never changes another), the module's test sources, screens and targets are removed, kept or rebound to the system's own objects, and its requirements are carried. The result lists the id map and every fixture outcome.
  • Editing groups (one per import to start with) can be redefined at any time, across imports and with your own elements; they never change the Project. transform moves any selection as one rigid body with an explicit frame and pivot, and reports every moved pose. An instance moved as a whole carries the instances nested in it, also one whose members were all deleted, so replacing that one later puts its members where the enclosing module has them.
  • Every change is atomic, and --op-id / --expect-base make retries safe: an operation repeated with the same --op-id is never applied twice. A command without --op-id is always applied as a new operation.
  • workspace update-project system.ws.json edited.project.json takes a Project you edited elsewhere; groups drop the objects it no longer has. A deleted member of an instance stays recorded as an edit of that instance: status lists it, and replace brings it back only with --discard-edits. The edits of the instances nested in an instance of a composed module (also the members a nested instance gained when it was replaced on its own) are edits of the enclosing instance too.
  • workspace replace writes the new revision's content exactly, however small a changed value; only an unedited component or observation plane whose module object the revision has unchanged keeps its exact content (so a whole-instance move or rotation never turns into an edit). A model, material or coating of the instance that an object outside it uses is never changed or removed: it stays with its content, the instance keeps it while its revision lacks it, and the result names it with those objects (kept_definitions). A revision with the same definition uses it as imported; one that defines its id differently is refused (ID_CONFLICT, used_by) until those objects use another definition. Replacing a nested instance by a revision that changes no object (also after a whole-instance rotation and a catalog re-pin), discarding an edit by replacing the nested instance, and bringing a nested instance back to the revision the enclosing module has (also after a whole-instance rotation made before) leave the instances enclosing it unedited. When the enclosing instance was moved or turned as a whole between the nested replacement and the return, the members that replacement changed stay edited until the enclosing instance is replaced with --discard-edits. A revision whose objects differ only by their catalog pin keeps every member exactly.
  • A system validation stays current until the physics or the targets change; regrouping does not affect it. A module's own validation never counts for the system.

The workspace file (opensci.optical_workspace) carries the organization and provenance; project.json inside it is an ordinary Project. Python (os_optics.composition), json_api.workspace_* and the browser Designer use the same operations: os-optics workspace --help lists the commands, help(os_optics.composition) the Python workflow.

Look at your result in 3D

os-optics view run opens the run's Optical Simulation View in your browser. The page is served only to this computer (127.0.0.1, a free port, a private random path) and stops with Ctrl+C. Every element sits exactly where your design puts it, in millimetres with +Z up, coloured by kind. Main beams are drawn thick; weak branches such as a leak through a dichroic coating or a ghost reflection are thin and dashed. Use the presets (iso, top, front, side, along the beam), click a part for its id, name, SKU and size, toggle classes and beams, and switch between English and Chinese.

  • --no-browser prints the local address.
  • --write-html view.html writes one self-contained file.
  • Glass, metal and the camera views are described in docs/VIEWER_MATERIALS.md: path_horizontal runs the main beam from left to right and plane_xy, plane_zx, plane_yz look square-on at a coordinate plane; the page also has Roll and Turn about beam.
  • --snapshot shots writes PNG images (orthographic, with labels, legend, axes and scale bar) plus scene_summary.json (parts by class with ids and bounding boxes in mm, beam paths, camera presets) and legend.md, for an Agent that works without a browser.
  • --section ALL adds the 2D layout of the system to --snapshot: section-ALL-XZ.png, .svg and .json, one true-scale section with every element on the axis, the stop, the image plane and the traced rays (coloured by source when several sources share a wavelength, so a source per field angle reads as the fields of a lens). A folded system gives one file per axis. --section ELEMENT refers the section to that element; repeat it or separate ids by commas.
  • --frame sets what those images show, for --section and --snapshot alike; the image takes its aspect within --size and the scale bar follows. optics fits the optical elements and the observation planes near them, without the source (the default of --section); scene fits everything, with the whole beam and far planes; --frame=X0,Y0,Z0,X1,Y1,Z1 fits that box of the design frame in millimetres. Without --frame, --snapshot shows the whole setup with the source and leaves far planes out.

From Python: os_optics.viewer.view_run("run"), render_snapshots("run", "shots"), scene_summary("run"). After commercial resolution, view_assembly(preview) shows the Experimental Lab Assembly from memory -- each part coloured by class on the breadboard with the beam through it; nothing of the assembly data is written to disk.

From design to Order and Detailed Assembly Drawing

Designing, simulating and Local Validation need no account. When a design is ready to buy, os-optics talks to the OpenSci Commercial Service for you: resolution, prices, the Order, the payment report, the Experimental Lab Assembly and the Detailed Assembly Drawing. The same workflow is available from Python (os_optics.client) and from the command line.

1. Point the engine at OpenSci and give your Agent its own token

OpenSci provides your Account owner credential; your OpenSci Account shows the OpenSci service URL with your Agent token. The owner credential (account:token:manage) manages tokens and nothing else; use it to give your Agent a token of its own:

export OPENSCI_API_URL=<OpenSci service URL from your OpenSci Account>
os-optics account token create --profile agent --label "my agent" --save --token-file owner-token.txt
os-optics account status        # organization, scopes, expiry, what the token allows

--save writes the new token to ~/.opensci/token, readable only by you, and does not print it. The engine finds the token in this order: --token-file PATH (commands) or the token= argument (Python), OPENSCI_TOKEN, OPENSCI_TOKEN_FILE, then ~/.opensci/token. A token is never a command-line value.

Profiles: agent lets your Agent resolve, view the Experimental Lab Assembly, create the Order, report the bank transfer, obtain the Assembly Detail and upload and read drawings; review resolves, views the Experimental Lab Assembly and reads Orders and drawings. os-optics account token rotate --issuer-token-file owner-token.txt replaces the token in use, os-optics account logout revokes it at once and deletes its token file, and account token list / account token revoke TOKEN_ID manage the others.

2. Resolve, inspect, order

os-optics project run project.json --out-dir run
os-optics resolve run                                  # resolution_id, Customer Purchase BOM, prices, assembly configuration
os-optics resolve run --optics-only                    # ... only the optical elements, no mounts or breadboard
os-optics resolve run --no-breadboard                  # ... on your own optical table (or --assembly-service)
os-optics resolve run --mechanics derived --platform none --mounting mounting.json  # an optical system with mounts
os-optics resolution show RESOLUTION_ID --locale zh-CN # the same for people (zh-CN or en), with the agreement
os-optics assembly preview RESOLUTION_ID --view        # the Experimental Lab Assembly: checked here, shown in 3D
os-optics account address list                         # the shipping addresses (mainland China), the default first
os-optics account address add --recipient 张三 --phone 13800138000 --province 广东省 --city 深圳市 \
    --district 南山区 --street "科技园路 1 号"                 # add one (the first becomes the default)
os-optics order create RESOLUTION_ID                   # the Order: Order Number, total, quotation, agreement, payment
os-optics order create RESOLUTION_ID --address ADDRESS_ID  # ... shipped to another address of the list
os-optics order create RESOLUTION_ID --additional-order    # ... a design your organization already ordered, again
os-optics order show ORDER_ID --locale zh-CN           # also --document quotation | contract

The resolution lists the OpenSci products of your system with quantities and current prices, and the assembly configuration: how each retained degree of freedom is provided (adjustable as designed, adjusted by hand where motorized adjustment was requested, or fixed -- with the reason when it is not as the design asks: design_deviations), the breadboard, the configuration notes, the clauses of the Order agreement (agreement; resolution show --locale renders them), the deliverables and the Orders your organization already has of the same design (design_orders; a further Order of it needs --additional-order).

What OpenSci supplies is set by independent order options, stated in the resolution as order_options:

Option resolve flag Values
Mechanical supply --mechanics derived (default): OpenSci selects the mount, post and post holder for each optic it supplies · none: the optical elements only
Platform --platform breadboard (default): the OpenSci breadboard, sized and drilled for the layout · none: your own optical table (M6 holes on a 25 mm grid)
Assembly --assembly customer (default): you assemble · opensci: OpenSci assembles and aligns the system on the OpenSci breadboard

The familiar flags are the same options: --optics-only is --mechanics none (no mounts, posts, holders or breadboard, no assembly service), --no-breadboard is --platform none and --assembly-service is --assembly opensci. The OpenSci assembly needs OpenSci mechanics and the breadboard; a combination that cannot be supplied is refused before anything is sent (PURCHASE_OPTIONS_CONFLICT, ASSEMBLY_SERVICE_REQUIRES_BREADBOARD). What is checked and delivered follows from the options: with mechanics and a laboratory layout the assembly is checked for collisions, a clear beam path and its fit on the board, and the deliverable is the Detailed Assembly Drawing; without mechanics it is the relative Optical Layout Drawing.

A Project with design_intent OPTICAL_SYSTEM has no laboratory layout: it takes no platform and no assembly service (otherwise DESIGN_INTENT_HAS_NO_LAB_ASSEMBLY). Buy it with --optics-only, or with OpenSci mounts per optic: --mechanics derived --platform none --mounting mounting.json, where mounting.json gives the beam height (above the surface the holders stand on) and optionally the angle of incidence of every OpenSci optic, {"L1": {"beam_height_mm": 125}, "M1": {"beam_height_mm": 125, "incidence_deg": 45}} (missing ones are refused with MOUNTING_INPUT_REQUIRED). OpenSci checks that each optic's mount, post and post holder fit together at that height and delivers a mounting sheet instead of an assembly drawing.

Your own declared optics are components you supply: they keep their place in the design and, in a complete system, their declared body reserves space; --spare SKU=N adds spare units of an OpenSci optic of the design. A token with the agent profile is the authorization to create the Order: order create creates it directly. It ships to the default address of your organization's address book, or to --address ADDRESS_ID; ask the person which address to use. With no address at all, order create is refused with SHIPPING_ADDRESS_REQUIRED (recovery ADD_SHIPPING_ADDRESS): add one with account address add (editing and deleting addresses is done on the OpenSci Account website). The Order keeps a copy of the address. Present the Order Number, the total, the shipping address and the bank-transfer instructions to the person who pays.

Ordering listed parts without a design

An Agent that knows which parts it needs orders them from a part list: OpenSci part numbers and quantities, with no Project. Optics come from the catalog (os-optics catalog list), mounts, posts, post holders and breadboards from the mechanical listing:

os-optics products mechanical --kind MOUNT             # OS-MECH-* part numbers with their facts (--json: the document)
os-optics resolve-parts parts.json                     # resolution_id, per-part prices, order_options (basis PART_LIST)
os-optics resolve-parts parts.json --mechanics none    # ... the listed parts alone, whatever the file chooses
os-optics resolution mounting-sheet RESOLUTION_ID --locale zh-CN   # per optic: mount, post, holder, steps, height
os-optics order create RESOLUTION_ID                   # ordered like a design

parts.json lists the lines (1 to 64, quantities 1 to 100) and the mechanical supply:

{"lines": [{"line_id": "L1", "opensci_sku": "OS-OPT-...", "quantity": 2},
           {"line_id": "B1", "opensci_sku": "OS-MECH-...", "quantity": 1}],
 "mechanical_supply": "DERIVED",
 "mounting": {"L1": {"beam_height_mm": 125, "retained_dofs": [], "incidence_deg": 0}}}

With "mechanical_supply": "NONE" (the default) OpenSci supplies exactly the listed parts; with "DERIVED" it selects a mount, post and post holder for each optic unit at the beam height its mounting entry states (measured from the surface the holders stand on), keeping the adjustment axes listed in retained_dofs (default none) at the angle of incidence (default 0 degrees, 45 for mirrors and plate splitters). When the first mount cannot reach the height the other compatible mounts are tried; otherwise the line is refused with MOUNTING_NOT_AVAILABLE (details: line_id, reason, the reachable range when known). A part number OpenSci does not supply is refused with PART_NOT_ORDERABLE (details.line_ids, recovery LIST_MECHANICAL_PRODUCTS). The parts ship unassembled; the agreement states that OpenSci supplies the listed products and has not checked that they work together as an optical system (with derived mechanics: that each optic's mount, post and holder fit together at the stated height). A breadboard in a part list is an ordinary line. The mounting sheet (resolution mounting-sheet / order mounting-sheet, JSON with English and Simplified Chinese text; --locale prints it as text, --out FILE writes it) gives each optic's chain, the assembly order and threads, the beam-height arithmetic and the adjustment axes.

To withdraw an Order -- only when the person asks, and only before a transfer is reported:

os-optics order withdraw ORDER_ID                      # the Order and its data are deleted

The Order, its quotation, agreement and shipping-address copy and (unless another Order uses it) the resolution are deleted; its Order Number is never used again. Tell the person it was withdrawn. To change a design after ordering: withdraw, run the design again, order again. After a reported transfer or a payment the service refuses (ORDER_NOT_WITHDRAWABLE, recovery CONTACT_OPENSCI): the person contacts OpenSci. Repeating order withdraw after a withdrawal answers ORDER_NOT_FOUND (the Order no longer exists). A resolution that was priced but never ordered is deleted after 7 days: run resolve again to order it later.

3. Pay, report, receive the Assembly Detail

Pay the full amount by corporate bank transfer with the exact Order Number as the transfer remark. When the person who paid says the transfer was made:

os-optics order report-transfer ORDER_ID

A report is not a confirmation: OpenSci confirms receipt of the payment. After that confirmation:

os-optics assembly drawing RESOLUTION_ID --out drawing --locale zh-CN   # SVG, PDF, result and manifest, uploaded
os-optics artifacts list RESOLUTION_ID
os-optics artifacts download SET_ID DRAWING_PDF --out drawing.pdf     # roles as 'artifacts show' lists them

Before the confirmation assembly drawing exits with code 75 and a JSON error whose recovery is WAIT_FOR_PAYMENT_CONFIRMATION (or CREATE_ORDER_FIRST when there is no Order yet). The drawing is written in English (default) or Simplified Chinese (--locale zh-CN); its title block names the Order Number and your organization.

Chinese text in the drawing PDFs (assembly_drawing.pdf, optical_layout.pdf) uses the standard Chinese PDF font STSong-Light (Adobe-GB1) without embedding a font file, so the PDFs stay small and identical on every computer. A viewer with Chinese font support renders it: Adobe Acrobat / Reader (with its Asian font pack, installed by default in Chinese editions), Chrome / Edge (PDFium), Firefox (pdf.js), macOS Preview, SumatraPDF and PyMuPDF. Poppler-based tools (pdftotext, Evince, Okular) need the poppler-data package for it. The SVG of the same sheet (assembly_drawing.svg, optical_layout.svg) needs no PDF font: open it in any browser when a PDF viewer shows no Chinese text.

An optics-only Order has no assembly drawing (mounts chosen per optic without a layout come with the mounting sheet, order mounting-sheet ORDER_ID): its deliverable is the Optical Layout Drawing, the relative layout of the optics (positions relative to a datum element, orientations, beam paths and spacings) to build your own mounting around:

os-optics order layout ORDER_ID --out layout --locale zh-CN    # optical_layout.svg / .pdf / manifest
os-optics layout draw optical-layout.json --out layout         # the same from a package file, offline

layout draw takes the Optical Layout Package (optical-layout.json) those commands save. Without one it exits 65 with OPTICAL_LAYOUT_PACKAGE_REQUIRED, naming the file and the commands that write it. A run directory has no package: its 2D layout is os-optics view run --section ALL --snapshot shots.

The same from Python

from os_optics.client import OpenSciClient
from os_optics.local import example_project, run_optical_project

client = OpenSciClient.from_environment()
run = run_optical_project(example_project(), output_dir="run")
resolution = client.submit(run)
rid = resolution["resolution_id"]
preview = client.assembly_preview(rid)                  # opened in memory
validation = client.validate_assembly(preview)
order = client.create_order(rid)["order"]
# client.withdraw_order(order["order_id"])  # only when the person asks, before a transfer is reported
# ... the customer pays; when they say the transfer was made:
client.report_bank_transfer(order["order_id"])
# ... after OpenSci has confirmed the payment:
drawing = client.assembly_drawing(rid, "drawing", preview=preview)
client.upload_assembly_results(rid, drawing=drawing, preview=preview)

Repeating a call is safe: submissions, Orders, payment reports and uploads carry an idempotency key derived from their input, so a repeat returns the first result (replayed: true). Every failure is an OpenSciApiError with code, message, details, recovery (what to do next), http_status and request_id.

The Order's assembly data on your machine

The Experimental Lab Assembly and the Assembly Detail are delivered encrypted for one Project and opened in memory; the engine writes no copy of them. It writes what you ask for: the run directory, the validation JSON (--out), the drawing files (--out DIR), snapshot images, and your token file (--save, readable only by you). Use the assembly data for viewing, checking and assembling that Project and for its Detailed Assembly Drawing; do not redistribute it. Keep your token out of chats, logs and shared files; if it leaks, os-optics account logout revokes it.

Exit codes of the commercial commands: 0 done, 1 diagnostic drawing, 64 usage / file error, 65 unusable local input or delivery, 67 refused by OpenSci, 69 OpenSci unreachable, busy or rate-limited (recovery RETRY_LATER: send the same request again after retry_after_seconds), 75 not available yet (e.g. the Assembly Detail before the payment is confirmed), 77 token missing, expired, revoked or without the needed scope, 78 OPENSCI_API_URL missing.

Send feedback to OpenSci

When something did not work while you were doing what the user needed, when a feature is missing, or when this documentation was missing or wrong, send it to OpenSci. Any OpenSci Account token works (Agent or Review profile):

os-optics feedback submit --category PROBLEM     --summary "order create refused although an address exists"     --details-file feedback.md     --error-code SHIPPING_ADDRESS_REQUIRED --request-id REQUEST_ID --resolution-id RESOLUTION_ID

--category is PROBLEM, FEATURE_REQUEST or DOCUMENTATION; --summary is one line (5 to 200 characters); --details TEXT or --details-file FILE (UTF-8, - for standard input; 1 to 8000 characters) says what the user needed, the commands you ran, what happened and what you expected. The engine version, the Python runtime and the active local catalog release are added; --order-id, --command, --locale and --agent NAME (default: OPENSCI_AGENT_NAME) add more context. It prints the receipt {"feedback_id", "received_at", "status": "RECEIVED", "replayed"}; sending the same feedback again returns the same receipt. From Python: client.submit_feedback(category=..., summary=..., details=..., context={...}).

One problem or suggestion per submission. Include no tokens, passwords, payment data or personal data beyond what the problem needs (text holding an Account token is refused before anything is sent; the service refuses credential-shaped text with FEEDBACK_INVALID, exit 67). Tell the user what you sent. At most 10 submissions per token per hour and 50 per organization per day: above that the service answers FEEDBACK_RATE_LIMITED (exit 69, recovery RETRY_LATER, retry_after_seconds).

Working with package files

os_optics.assembly, os_optics.assembly_drawing and os_optics.client_artifacts also work on package objects and files directly: load_assembly_preview(source) verifies a package (manifest, digests, schema, geometry) and validate_assembly_layout(package) runs the local assembly check (AssemblyLocalValidationV1); generate_assembly_drawing(preview, detail) draws; validation_artifact_upload / drawing_artifact_upload build the upload (manifest + exact file bytes) that the artifact routes accept. The os-optics assembly verify | validate | draw | verify-drawing commands do the same on files.

Low-level Engine API

Below the design workflow is the stateless Engine itself: validate(request) on a ValidationRequest (3D geometric optics, Gaussian beams with x/y-independent q-parameters, Jones polarization, analytic two-beam interference, 2.5D layout, control / recoverability analysis), os-optics validate request.json --pretty on the command line. Build a scene programmatically with the factories in os_optics.builders:

from os_optics import builders as B
from os_optics.contracts import ProjectSnapshot, Target
from os_optics import ValidationRequest, validate

project = ProjectSnapshot(
    components=(
        B.laser("L", wavelength_nm=532.0, waist_radius_mm=1.0, position=(0, 0, 20), direction=(1, 0, 0)),
        B.thin_lens("Lens", focal_length_mm=100.0, clear_radius_mm=12.7, position=(10, 0, 20), axis=(1, 0, 0)),
    ),
    observations=(B.observation_plane("focus", position=(110, 0, 20), normal=(1, 0, 0), radius_mm=5.0),),
    targets=(Target(target_id="w", metric="beam_radius_mm", observation_id="focus", maximum=0.05),),
)
result = validate(ValidationRequest(project=project))
print(result.status.value, result.targets[0].value)    # the focused 1/e² beam radius in mm

Public API

Object Purpose
os_optics.local local pure-optical design workflow: public catalog, run_optical_project, run directory, verify_run_artifacts, update_project_catalog; Project edit operations: project_ops.update_project / json_api.update_project; run freshness: freshness.run_freshness / json_api.run_freshness; declared optical models: compile_model (the one compile path), declared_copy, json_api.catalog_model
os_optics.composition reusable layouts: workspaces, import_layout, select, transform, editing groups, replace_instance, batch, export_module, run_workspace, workspace_status
os_optics.results the scientific results of a run: load_run, target outcomes (targets()), monitors, beams, paths, power budget, rays, wavefront, irradiance map, metric descriptors, CSV / JSONL / JSON / NPZ export
os_optics.catalog the local catalog store: status, check, sync, verify, import_release
os_optics.viewer first-party local 3D viewer: view_run, view_assembly, render_snapshots, scene_summary
os_optics.client OpenSciClient: the commercial workflow with your Account token (resolution, Order, payment report, assembly data, drawing, artifacts, tokens)
os_optics.optical_view the Optical Simulation View builder and GLB writer (used by run_optical_project)
os_optics.assembly local assembly check of an Assembly Preview Package
os_optics.assembly_drawing local Detailed Assembly Drawing (SVG / PDF + result / manifest) from a preview + detail package
os_optics.protected_delivery open an encrypted assembly delivery in memory (extra [protected]); used by the client
os_optics.client_artifacts validation_artifact_upload / drawing_artifact_upload: the artifact upload (manifest + files, multipart parts)
validate(request) / validate_json(text) pure function: request -> ValidationResult (never raises for physical / contract validation outcomes represented by the result model)
validate_with_ray_paths(request) / trace_ray_paths(request) the same single run plus a RayPathTrace: chief and pupil sample ray polylines of every traced path; input_hash / trace_hash equal the run's manifest
ValidationRequest, ProjectSnapshot, EngineConfiguration, NumericalConfig inputs (Pydantic v2, JSON friendly)
ValidationResult, ValidationIssue, ValidationManifest outputs (finite numbers only, structured issues, hashes)
os_optics.builders element factories producing contract objects
os_optics.compare.compare_results cross-runtime comparison within cross_runtime_rel_tol/abs_tol
os-optics doctor / capabilities / catalog / project / native / workspace / interop / results / export / view / layout / account / products / resolve / resolve-parts / resolution / order / assembly / artifacts / feedback / schema / validate / compare command line: every top-level command (os-optics --help, os-optics COMMAND --help)

Status vocabulary: PASS, WARNING, FAIL, UNKNOWN, NOT_APPLICABLE, OUT_OF_MODEL_SCOPE. Design CLI exit codes: 0 PASS/WARNING/NOT_APPLICABLE, 1 FAIL, 2 UNKNOWN, 3 OUT_OF_MODEL_SCOPE, 64 usage error, 65 a Project or catalog that cannot be simulated as given, or an assembly package that does not verify, 66 project verify found an inconsistent run directory / assembly verify an invalid package.

The "never raises" guarantee above applies to the validate() / validate_json() entry points: physical and contract-validation outcomes are always represented by the ValidationResult model, never raised as exceptions. Direct construction of the frozen Pydantic contract models (e.g. builders.laser(wavelength_nm=float("nan")) or SourceSpec(...)) can still raise a Pydantic ValidationError before those entry points are called, because the contract models deliberately set allow_inf_nan=False. Pass such input through validate_json (or the CLI) to receive it as a structured FAIL / INVALID_INPUT result instead.

Conventions

Right-handed 3D, lengths mm, wavelengths nm (vacuum), angles degrees at the API, power W, w = 1/e² intensity radius. Element local +z = optical axis; surface normals are explicit; Jones vectors live in a transported transverse basis (e1, e2), e1 × e2 = d. E ∝ exp[i(k·r − ωt)]. Every numerical tolerance is a documented field of NumericalConfig (help(os_optics.NumericalConfig)); results record the configuration version they were computed with. input_hash / result_hash are SHA-256 integrity and reproducibility digests (results quantized to 9 significant digits): they carry no secret and prove nothing about who computed a result.

License

OpenSci Engine is proprietary software, distributed free of charge through PyPI under the OpenSci Engine License (LicenseRef-OpenSci-Engine; the full text is LICENSE.txt, with NOTICE.txt, in every wheel and source distribution). It is not open-source software.

  • Free to download and use, including for research, education, internal engineering and commercial research and development, and by your own AI agents and tools.
  • Your outputs are yours: designs, results, drawings, reports and bills of materials you produce with it can be used, published and shared freely.
  • Without written authorization from OpenSci the software itself may not be redistributed or mirrored, resold or sublicensed, repackaged into another SDK or package, or offered as the core of a competing software product or service, and its notices may not be removed.
  • Third-party dependencies and bundled components keep their own licenses.

Metadata

Release files for os-optics 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for os-optics 0.12.0
File Size Uploaded Provenance Actions
os_optics-0.12.0.tar.gz 1.9 MB Without provenance Details for os_optics-0.12.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for os-optics 0.12.0
File Interpreter ABI Platform Actions
os_optics-0.12.0-py3-none-any.whl Python 3 none any Details for os_optics-0.12.0-py3-none-any.whl

Total release size: 4.0 MB

Release files / os_optics-0.12.0.tar.gz

Download URL os_optics-0.12.0.tar.gz
Size 1.9 MB
Tags Source
SHA-256 checksum
How to use checksums
8aa80e3c076aefaac97ddb98a98587af4cc41fe51ba4fd5823d76f911c830150
BLAKE2b-256 checksum
How to use checksums
ac7f94b502c8428aa54e28f92670cc1e57c7ce4e337bec3a5a363b45b3c512ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / os_optics-0.12.0-py3-none-any.whl

Download URL os_optics-0.12.0-py3-none-any.whl
Size 2.1 MB
Tags Python 3
SHA-256 checksum
How to use checksums
d2170b66515ab5ed7d3b75e4da827c02bfe002586bf858be39cc423b7c49ca92
BLAKE2b-256 checksum
How to use checksums
50f50518bbdd7fcffaafb2bc11c564e2e7a819dcb0e1bbaa4f2696fea20e3146
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Security for os-optics 0.12.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page