Metadata-Version: 2.5
Name: opensci-engine
Version: 0.3.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-tech.com/
Author: OpenSci
License: Proprietary
Keywords: gaussian beam,opensci,optical design,optics,polarization,ray tracing,simulation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: Other/Proprietary License
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-tech.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.

`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 bundled catalog, and whether
`OPENSCI_API_URL` and an Account token are set up. `ready.local_design: true` means you can start designing;
`next_steps` lists the exact commands for anything missing. The token itself is never shown. 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.

## 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) and `polarization` (waveplate + polarizer). Each is built from the bundled catalog by query.

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 public optical catalog bundled with this package
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, issues |
| `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, power, 1/e² waist radius and position, M², polarization); 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 by a numerically controlled actuator). 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. 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.

## The catalog and the model basis

`opensci-engine catalog list` / `load_public_catalog()` give the bundled OpenSci optical catalog: lenses
(plano-convex / -concave, bi-convex / -concave, meniscus, best-form, cemented achromatic doublets), flat and concave
mirrors, windows, plate beamsplitters, dichroic mirrors, edge / band / notch / neutral-density filters, 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 vs simulation-only.** Every product states its `commercial_readiness`: `ORDERABLE` (OpenSci supplies it in
an assembled system) or `SIMULATION_ONLY` (available for design and simulation; a Project that uses one cannot be
ordered). `opensci-engine catalog list` shows the readiness in each row.

**Model basis.**

* Surfaces are plane or spherical; lenses and windows are traced surface by surface through their prescription with
  the dispersion of their glass (`SURFACE_PRESCRIPTION`); mirrors and beamsplitters answer with their specified
  coating envelope (`SPECIFICATION_ENVELOPE`); filters, waveplates and polarizers are ideal thin elements
  (`IDEAL_THIN_ELEMENT`, Jones matrices).
* 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`.

## 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 and the Commercial Service address. 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=<your OpenSci service address>
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 resolution show RESOLUTION_ID --locale zh-CN # the same for people (zh-CN or en)
opensci-engine assembly preview RESOLUTION_ID --view        # the Experimental Lab Assembly: checked here, shown in 3D
opensci-engine order create RESOLUTION_ID                   # the Order: Order Number, total, quotation, agreement, payment
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
numerical control was requested, or fixed), the breadboard, and configuration notes. A token with the `agent` profile
is the authorization to create the Order: `order create` creates it directly. Present the Order Number, the total and
the bank-transfer instructions to the person who pays.

### 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   # SVG, PDF, result and manifest in drawing/, uploaded
opensci-engine artifacts list RESOLUTION_ID
opensci-engine artifacts download SET_ID ROLE --out FILE
```

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 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"]
# ... 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, 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.

### 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` |
| `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` / `view` / `account` / `resolve` / `resolution` / `order` / `assembly` / `artifacts` / `schema` / `validate` / `compare` | command line (`opensci-engine --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.
