Metadata-Version: 2.5
Name: opensci-engine
Version: 0.8.0
Summary: OpenSci local optical design engine: public optical catalog, multi-element optical Projects, deterministic offline simulation, Local Validation and a 3D Optical Simulation View.
Project-URL: Homepage, https://opensci-optics.com/
Author: OpenSci
License-Expression: LicenseRef-OpenSci-Engine
License-File: LICENSE.txt
License-File: NOTICE.txt
Keywords: gaussian beam,opensci,optical design,optics,polarization,ray tracing,simulation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: numpy<3,>=2.3
Requires-Dist: pydantic<3,>=2.11.5
Requires-Dist: scipy<2,>=1.16
Requires-Dist: shapely<3,>=2.1
Provides-Extra: protected
Requires-Dist: cryptography<51,>=50.0.1; extra == 'protected'
Provides-Extra: test
Requires-Dist: cryptography<51,>=50.0.1; extra == 'test'
Requires-Dist: pytest-cov>=5; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# opensci-engine

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 `OUT_OF_MODEL_SCOPE` or `UNKNOWN` -- never `PASS`.
* 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):

```bash
uv tool install "opensci-engine[protected]"
opensci-engine doctor
```

or run without installing: `uvx --from "opensci-engine[protected]" opensci-engine doctor`.

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

```bash
python --version                                  # 3.11 or newer
python -m venv .venv
.venv/bin/python -m pip install "opensci-engine[protected]"     # Windows: .venv\Scripts\python -m pip install ...
.venv/bin/opensci-engine doctor                                 # Windows: .venv\Scripts\opensci-engine doctor
```

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

**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:

```bash
python -m pip install --upgrade "opensci-engine[protected]"
opensci-engine catalog sync
opensci-engine project update-catalog project.json --out project.json   # add --include-changed if it reports "unverified"
opensci-engine project run project.json --out-dir run
opensci-engine 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.

`opensci-engine doctor` (or `python -m opensci_engine 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 (`opensci-engine catalog check` asks the catalog
service, `opensci-engine 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 `opensci-engine 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.** `opensci-engine 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 opensci_engine 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 `opensci-engine 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 `opensci-engine 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). `opensci-engine 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.

```bash
opensci-engine catalog sync                     # first run (or let the first design command do it)
opensci-engine catalog status                   # active release, stored releases, last check
opensci-engine catalog import catalog.json      # an offline computer: use a release file you copied
opensci-engine catalog use 2026-09-30.3         # make a stored release the active one (no network)
```

## Quick start

```bash
opensci-engine catalog list --category LENS --wavelength-nm 780 --limit 20   # products usable at 780 nm (paged rows)
opensci-engine catalog show OS-OPT-...                            # one product: specifications, surfaces, band, geometry
opensci-engine project example --kind beam_expander > project.json   # a complete two-lens reference design to copy and edit
opensci-engine project run project.json --out-dir run             # simulate; exit code 0 PASS, 1 FAIL, 2 UNKNOWN, 3 OUT_OF_MODEL_SCOPE
opensci-engine view run                                           # the 3D Optical Simulation View in a local browser page
opensci-engine view run --snapshot shots                          # or PNG views + scene_summary.json, without a browser
opensci-engine 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.
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 from Python:

```python
import tempfile
from opensci_engine.local import example_project_of_kind, load_public_catalog, run_optical_project, verify_run_artifacts
from opensci_engine.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
```

`run.status` is the outcome, `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 (`opensci-engine 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 `opensci_engine.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 (`opensci-engine schema optical-scene`) |
| `optical_scene.glb` | the same view as glTF 2.0: `opensci-engine 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.** `PASS` / `WARNING`: every target met (`WARNING` lists issues worth reading; commercial resolution accepts a run whose Local Validation is `PASS`, so resolve the listed issues first). `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 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 `opensci-engine project example` (or `example_project().payload()` in Python)
and edit it; `opensci-engine schema project` prints the JSON Schema and `print(opensci_engine.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` (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); 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 (`opensci_engine.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 `opensci-engine 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.

## Scientific results and export

A run directory is the complete scientific record of one Engine execution, and `opensci_engine.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.

```python
import tempfile
from pathlib import Path
from opensci_engine.local import example_project_of_kind, run_optical_project
from opensci_engine.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:

```bash
opensci-engine results describe run                    # identity, status, available results, conventions, files
opensci-engine results monitor run focal_plane         # one plane: every arriving beam's state, coherent sums, targets
opensci-engine results wavefront run --monitor focal_plane --path 'SRC#0' --reference sphere --center ray_focus
opensci-engine results metrics --name waist_distance_x_mm   # unit, definition, sign convention, validity
opensci-engine export arrivals run --format csv --out arrivals.csv
opensci-engine 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 (`opensci-engine 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)` (`opensci-engine 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. OpenSci 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.

## Spacing lenses: 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`.

```python
from opensci_engine.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: `opensci-engine project paraxial project.json --components L1 L2 --wavelength-nm 780 --solve afocal`, or
`--components L1 --wavelength-nm 780 --solve collimate --source SRC`. 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. `opensci-engine 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`.
`opensci-engine interop show ID` (for example `interop show quantum-photonics`; Python:
`opensci_engine.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. `opensci-engine interop list --status VERIFIED_EXAMPLE` lists the entries
with an executed example, and `opensci-engine 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.
* **Outside the exposure model:** light recirculating through closed paths of three or more surfaces (including a stack
  of three or more reflecting surfaces whose reflections chain, and a periscope's windows through its folds); light
  trapped by total internal reflection; light that refraction turns back onto a lens or a refracting surface it already
  passed, before it is reflected again. 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; the Local Validation reports a traced beam
  that meets it beyond its published data (`OUT_OF_MODEL_SCOPE`).
* **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
  leaves the surface no upper bound unless a rating of the same quantity that applies as stated or with a larger test
  beam has a value no larger.
* **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()` (`opensci-engine 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), the peak fluence. 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. Outside the exposure model (as heat is): light recirculating through closed paths of three or
more surfaces (including a stack of three or more reflecting surfaces whose reflections chain, and a periscope's windows
through its folds); light trapped by total internal reflection; 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`, which is neither verified nor keeps a rating from applying):
* regime, wavelength within 2 %, pulse duration 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) 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 stands for 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 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, read as the number of pulses each tested site received (S-on-1): a Project states no pulses per site,
  so it is not verifiable (`DESIGN_INPUT_MISSING`);
* 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 rating's `applicability` follows: `AS_RATED` (every stated condition verified: `WITHIN_RATING` when the exposure's
upper bound is within the rating, `EXCEEDS_RATING` when its lower bound is above it, else `UNKNOWN`),
`THRESHOLD_AT_LEAST_RATED` (only `WITHIN_RATING` is decided), `THRESHOLD_AT_MOST_RATED` (only `EXCEEDS_RATING`),
`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 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 leaves none unless a rating of the same quantity that applies as stated
or with a threshold at least rated has a value no larger; where every such rating has a larger value 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 / rating
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

`opensci-engine 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. `opensci-engine catalog list` shows the readiness in each row and, for
an ORDERABLE product, its OpenSci mounting (`--readiness ORDERABLE`, `--mounting AVAILABLE` filter by them).

**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 (propagation through apertures and pinholes, PSF, MTF, far-field patterns), stray light and
  scattering are reported `OUT_OF_MODEL_SCOPE`.

## 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,
    and the axicon cone term `linear_sag_coefficient`; 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.
    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, and a target on a metric the model was not
    validated for, is `OUT_OF_MODEL_SCOPE`. 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;
  * `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 reported
    out of model scope;
  * `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, `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; the pulse count is read as the number of pulses each tested site received
  (S-on-1). 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 makes the
  Local Validation `OUT_OF_MODEL_SCOPE` (`COATING_WAVELENGTH_OUTSIDE_BANDS`, `COATING_AOI_OUTSIDE_RANGE`).
* 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`; 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 `opensci-engine 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.

```python
from opensci_engine.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.** `opensci-engine 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(opensci_engine.prescription.csv_table)`.
* **Build a supplier-independent component.** `opensci-engine 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, variable neutral-density filters, lenses of a tabulated response,
  telescopes and a 4f relay); `opensci-engine native describe CLASS` gives its parameters and `opensci-engine
  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. `opensci-engine 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.** `opensci-engine view run --section OBJ --snapshot shots` adds an exact true-scale section of an
  element (or a whole unit along its axis): each surface's profile, the glass between them, clear-aperture marks and the
  run's rays in that plane. The 3D view lists every surface of the selected element to show, hide or click.
* **Export a linear optical network.** `opensci-engine 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`);
  `opensci-engine schema linear-optical-network` prints the format.

The same functions are in `opensci_engine.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.

## 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.

```
opensci-engine workspace init fold.json --module fold.descriptor.json -o fold.ws.json   # mark test fixtures, interfaces
opensci-engine workspace run fold.ws.json                                               # the module on its own
opensci-engine workspace export fold.ws.json --descriptor fold.descriptor.json -o fold.module.json
opensci-engine workspace init system.json -o system.ws.json
opensci-engine workspace import system.ws.json fold.module.json --key a --rebind SRC=LASER --op-id import-a
opensci-engine workspace import system.ws.json fold.module.json --key b --rebind SRC=LASER --position 0,100,0 --op-id import-b
opensci-engine workspace transform system.ws.json --instance b --axis 0,0,1 --angle 90 --pivot component:b_M1 --op-id turn-b
opensci-engine workspace run system.ws.json --out-dir run
opensci-engine 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 (`opensci_engine.composition`), `json_api.workspace_*` and the browser Designer use the same
operations: `opensci-engine workspace --help` lists the commands, `help(opensci_engine.composition)` the Python workflow.

## Look at your result in 3D

`opensci-engine 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.
* `--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.

From Python: `opensci_engine.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, `opensci-engine` 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 (`opensci_engine.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:

```bash
export OPENSCI_API_URL=<OpenSci service URL from your OpenSci Account>
opensci-engine account token create --profile agent --label "my agent" --save --token-file owner-token.txt
opensci-engine 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. `opensci-engine account token rotate --issuer-token-file owner-token.txt`
replaces the token in use, `opensci-engine 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

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

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

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

```json
{"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:

```bash
opensci-engine 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:

```bash
opensci-engine order report-transfer ORDER_ID
```

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

```bash
opensci-engine assembly drawing RESOLUTION_ID --out drawing --locale zh-CN   # SVG, PDF, result and manifest, uploaded
opensci-engine artifacts list RESOLUTION_ID
opensci-engine 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:

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

### The same from Python

```python
from opensci_engine.client import OpenSciClient
from opensci_engine.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, `opensci-engine 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):

```bash
opensci-engine 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

`opensci_engine.assembly`, `opensci_engine.assembly_drawing` and `opensci_engine.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 `opensci-engine 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), `opensci-engine validate request.json --pretty` on the command line. Build a
scene programmatically with the factories in `opensci_engine.builders`:

```python
from opensci_engine import builders as B
from opensci_engine.contracts import ProjectSnapshot, Target
from opensci_engine 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 |
|---|---|
| `opensci_engine.local` | local pure-optical design workflow: public catalog, `run_optical_project`, run directory, `verify_run_artifacts`, `update_project_catalog`; declared optical models: `compile_model` (the one compile path), `declared_copy`, `json_api.catalog_model` |
| `opensci_engine.composition` | reusable layouts: workspaces, `import_layout`, `select`, `transform`, editing groups, `replace_instance`, `batch`, `export_module`, `run_workspace`, `workspace_status` |
| `opensci_engine.results` | the scientific results of a run: `load_run`, monitors, beams, paths, power budget, rays, wavefront, irradiance map, metric descriptors, CSV / JSONL / JSON / NPZ export |
| `opensci_engine.catalog` | the local catalog store: `status`, `check`, `sync`, `verify`, `import_release` |
| `opensci_engine.viewer` | first-party local 3D viewer: `view_run`, `view_assembly`, `render_snapshots`, `scene_summary` |
| `opensci_engine.client` | `OpenSciClient`: the commercial workflow with your Account token (resolution, Order, payment report, assembly data, drawing, artifacts, tokens) |
| `opensci_engine.optical_view` | the Optical Simulation View builder and GLB writer (used by `run_optical_project`) |
| `opensci_engine.assembly` | local assembly check of an Assembly Preview Package |
| `opensci_engine.assembly_drawing` | local Detailed Assembly Drawing (SVG / PDF + result / manifest) from a preview + detail package |
| `opensci_engine.protected_delivery` | open an encrypted assembly delivery in memory (extra `[protected]`); used by the client |
| `opensci_engine.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) |
| `opensci_engine.builders` | element factories producing contract objects |
| `opensci_engine.compare.compare_results` | cross-runtime comparison within `cross_runtime_rel_tol/abs_tol` |
| `opensci-engine doctor` / `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 (`opensci-engine --help`, `opensci-engine 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(opensci_engine.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.
