Metadata-Version: 2.5
Name: opensci-engine
Version: 0.2.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.14
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Typing :: Typed
Requires-Python: >=3.14
Requires-Dist: numpy<3,>=2.3
Requires-Dist: pydantic<3,>=2.11
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 a pure-optical system from OpenSci's public optical catalog, simulate it
with a deterministic physics engine, and get a structured **Local Validation** record and a 3D **Optical Simulation
View** -- all on your own computer, offline and headless, with no OpenSci server, browser or account. This is OpenSci
"Mode A", the headless Python route (the browser Designer, "Mode B", runs the same workflow in a web page); the product
site is <https://opensci-tech.com/>.

```bash
python -m pip install opensci-engine                  # Python 3.14
python -m pip install "opensci-engine[protected]"     # adds opening protected Assembly Preview / Detail deliveries
```

* Pure Python on Python **3.14**; depends only on `numpy`, `scipy`, `pydantic>=2` and `shapely` (the `protected` extra
  adds `cryptography`). No compiler, GPU or network access is needed, and nothing is sent anywhere.
* 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`.

## Who does what

* **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.
* **Physics runs locally.** Optical simulation, Local Validation, the 3D view, the local assembly check and the Detailed
  Assembly Drawing are all computed on your computer by this package. OpenSci's servers never run them.
* **Commercial steps are not in this package.** An Account token, the private Assembly Resolution (mounts, posts and
  breadboard chosen by OpenSci), the Customer Commercial BOM, the Order, bank transfer and the protected deliveries go
  through an OpenSci Commercial API instance you have been given access to. Its `GET /api/v1` capabilities document
  describes the authentication, scopes, ordered workflow and payment rules; this package only provides the local steps
  that workflow asks for (see "Commercial flow in brief" below).

## Quick start

From a shell, after `python -m pip install opensci-engine`:

```bash
opensci-engine catalog list --category LENS --wavelength-nm 780 --limit 20   # public products usable at 780 nm (paged rows)
opensci-engine catalog show OS-OPT-...                            # one product (a SKU from the list): 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 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

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, "| f =", lens.specifications.focal_length_mm.value, "mm")

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])
print(sorted(run.artifacts.paths))                           # the run directory, including optical_scene.glb
assert verify_run_artifacts(run.artifacts.directory) == ()
```

`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, public optical SKUs + catalog version + simulation model, retained DOF intent, target outcomes, simulation and validation status |
| `optical_scene.json` | the Optical Simulation View as renderer-neutral JSON (`opensci-engine schema optical-scene`) |
| `optical_scene.glb` | the same view as a glTF 2.0 file: open it in any glTF viewer (e.g. Blender, the three.js editor, an online glTF viewer) |

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 optical elements (public neutral geometry), the source, the Engine-traced
chief and pupil rays, and observation / target planes with the beam footprints -- it is not a laboratory assembly.
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 withholds that beam's Gaussian numbers
(`GAUSSIAN_ABERRATION_DOMINATED`, `OUT_OF_MODEL_SCOPE`); smaller aberrations are not flagged, and `pupil_rms_radius_mm`
gives the rays' own spot.

**Reading the outcome.** `PASS` / `WARNING`: every target met (`WARNING` lists issues worth reading). `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 can answer faithfully (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 from public data 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 only -- catalog products by public `OS-OPT-*` SKU and user-defined 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 -- design intent, not hardware). 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, so keep them neutral. Mounts, posts, stages, breadboards and suppliers are never part of a design.

## What the catalog and the simulation cover

The published catalog holds lenses (plano-convex / -concave, bi-convex / -concave, meniscus, best-form, cemented
achromatic doublets), flat and concave mirrors, windows, plate beamsplitters, dichroic mirrors and edge / band / notch /
neutral-density filters, half- and quarter-wave plates and linear polarizers, plus user-defined Gaussian sources. Every
product is published as data -- surfaces, media with their dispersion, coatings with their bands and angle range, or a
Jones retarder / polarizer -- and simulated by the Engine's general physics (`opensci_engine.local.compile`): multi-lens
telescopes, relays, folded and mixed reflective / transmissive paths, several wavelengths and polarization all compose
normally. Anything the runtime cannot represent faithfully is refused or reported `OUT_OF_MODEL_SCOPE`, not approximated
silently. The catalog is supplier-neutral: products carry OpenSci identities only.

**Simulation-only vs orderable.** Every product states its `commercial_readiness`. A **simulation-only** product
(`SIMULATION_ONLY`) can be designed with and simulated, but OpenSci's Assembly Resolution has no mechanical realization
for it, so a Project that uses one cannot be ordered. Every other product is one OpenSci can realize and quote through
the commercial flow. `opensci-engine catalog list` shows the readiness in each row.

**Model limits.** Worth knowing before trusting a number:

* Surfaces are plane or spherical; no aspheric, cylindrical, freeform, gradient-index or birefringent elements.
* Rays follow each beam's chief ray plus deterministic pupil samples; stray light, ghost images beyond one optional
  Fresnel reflection per interface, multiple internal reflections and scattering are not modelled.
* Gaussian beams are paraxial (x/y-independent q-parameters, untwisted astigmatism); diffraction from apertures,
  pinholes and spatial filters is not propagated; wave-optics quantities (PSF, MTF, far-field patterns) are reported
  `OUT_OF_MODEL_SCOPE`.
* Each product's simulation model states its `fidelity`: lenses and windows are traced surface by surface
  (`SURFACE_PRESCRIPTION`); mirrors and beamsplitters answer with their specified coating envelope
  (`SPECIFICATION_ENVELOPE`); filters, waveplates and polarizers are ideal thin elements (`IDEAL_THIN_ELEMENT`).
* Polarization is fully polarized Jones calculus (no depolarization); interference is an analytic two-beam model;
  materials have no temperature dependence.
* Results are compared with analytic and independent references, not with a commercial optical design program or with
  laboratory measurements.

## Check a resolved assembly locally

After a commercial submission, OpenSci resolves the mechanics (mounts, posts, holders, breadboard) privately and sends
your computer an **Assembly Preview Package** for that one Project: `manifest.json`, `scene.json` and one proxy
`geometry/<part_ref>.glb` per part, with OpenSci identities only. `opensci_engine.assembly` checks that answer on your
computer; it never re-selects parts or poses:

```python
from opensci_engine.assembly import load_assembly_preview, validate_assembly_layout

package = load_assembly_preview("assembly-preview.zip")   # verified first: manifest, digests, schema, geometry
result = validate_assembly_layout(package)                 # AssemblyLocalValidationV1
print(result.status.value, result.assurance.clearance_claim.value)
for issue in result.issues:
    print(issue.code.value, issue.part_refs, issue.message)
```

`load_assembly_preview` and `validate_assembly_layout` also accept the package bytes or a verified package object, so a
protected delivery can be checked in memory (next section). The command line reads a package from disk:
`opensci-engine assembly validate assembly-preview.zip --out assembly-validation.json` (exit code = status). A package
that does not verify is refused before any geometry runs. The checks: scene transforms, proxy mesh closure, support
attachment (mates close, nothing floats), breadboard containment (your own light source is excluded), fixation holes,
expected engagement of each part with its support (inside the interface allowance), gross collision between parts not
expected to touch, the nominal beam axis, and geometry fidelity. It is a **gross check on preview proxies**: never a
manufacturing clearance, tolerance, thread, fastener, strength or optical-alignment verification (`result.assurance`).

## Open a protected delivery and upload what you computed

The OpenSci Commercial API delivers the Assembly Preview Package, and once released the Assembly Detail Package,
encrypted: an AES-256-GCM envelope plus a short-lived decryption grant, wrapped in one delivery document
(`{"schema": "opensci.commercial_api.protected_delivery", "schema_version": "1", "delivery": "ASSEMBLY_PREVIEW" |
"ASSEMBLY_DETAIL", "commercial_resolution_id": ..., "envelope": {...}, "grant": {...}}`). Opening one needs the
`protected` extra: `python -m pip install "opensci-engine[protected]"`.

`opensci_engine.protected_delivery` opens a delivery document in memory and returns the verified package;
`opensci_engine.client_artifacts` wraps what your computer computed into the upload the artifact store accepts
(manifest + exact file bytes, every binding taken from the Engine outputs):

```python
from pathlib import Path

from opensci_engine.assembly_drawing import generate_assembly_drawing
from opensci_engine.client_artifacts import drawing_artifact_upload, validation_artifact_upload
from opensci_engine.protected_delivery import open_detail_delivery, open_preview_delivery

preview = open_preview_delivery(Path("preview-delivery.json").read_bytes())   # AssemblyPreviewPackageV1, verified
detail = open_detail_delivery(Path("detail-delivery.json").read_bytes(), preview=preview)
drawing = generate_assembly_drawing(preview, detail)       # runs the local assembly validation first
uploads = [validation_artifact_upload(preview, drawing.validation), drawing_artifact_upload(drawing)]
for upload in uploads:
    print(upload.manifest.artifact_kind.value, upload.manifest.artifact_set_digest)
    fields = upload.multipart_fields()   # [("manifest", ("manifest.json", ...)), ("file", (filename, bytes, media_type)), ...]
```

Every refusal is a `ProtectedDeliveryError` with a stable `code` (`GRANT_EXPIRED`, `BINDING_MISMATCH`,
`DECRYPTION_FAILED`, ...) whose message never carries the key; without `cryptography` it is `CRYPTO_UNAVAILABLE`,
naming the extra. `import opensci_engine` and the design workflow never need `cryptography`. The upload is
`multipart/form-data` with one `manifest` part and one `file` part per manifest file; the organization comes from your
authenticated request, never from the manifest.

**Protected data, truthfully:**

* `open_preview_delivery` / `open_detail_delivery` write nothing: the decrypted packages exist only in the returned
  objects. Keep them that way -- do not save the decrypted packages, and keep grants out of logs, files, error reports
  and URLs. The assembly command line reads packages from disk, so use the Python API above for protected deliveries.
* Every package carries its usage terms (`preview.manifest.usage`, `detail.detail.usage`): this one Project only, no
  persistent decrypted copy, no bulk extraction, supplier-identity recovery or reverse engineering. The Commercial API
  instance's `GET /api/v1` capabilities document describes the protected-delivery usage as well.
* The Detailed Assembly Drawing is declared a separate customer deliverable: its four files (SVG, PDF, result,
  manifest), which `drawing.write(directory)` or `opensci-engine assembly draw` writes where you ask, do not inherit
  the no-persistent-plaintext rule. The local assembly validation result is the other file the upload carries. The
  terms give no separate status to anything else derived from a package (renders, extracted meshes, dimensions), so
  treat it like the package itself.
* The grant's validity window is enforced by the opener and by the server's delivery policy. It is **not** cryptographic
  revocation: whoever keeps an envelope together with its grant can still decrypt it after the window closes, with any
  AES-GCM implementation. That is why grants must never be persisted. This is practical deterrence, not DRM.

## Commercial flow in brief

The commercial steps run against the Commercial API instance; this package supplies the local parts (marked *local*):

1. *local* -- design, simulate and produce the Local Validation record of the Project;
2. submit the Project and its record with an Account token; OpenSci resolves the mechanics privately and returns the
   Customer Commercial BOM and its disclosures (for example which degrees of freedom are delivered, and any
   mechanical compromise), and the protected Assembly Preview (*local*: open it and check the assembly);
3. an Agent holding an order-capable token creates the Order directly; there is no separate quote-acceptance step;
4. the Order carries its Order Number, the quotation, the contract and the corporate bank-transfer instructions;
5. the customer makes the transfer with the Order Number as the remark and may report it; a report is not a payment
   confirmation;
6. OpenSci finance confirms receipt of the payment; only then is the protected Assembly Detail released (*local*: open
   it, generate the Detailed Assembly Drawing and upload the drawing and validation artifacts).

## 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.optical_view` | the Optical Simulation View builder and GLB writer (used by `run_optical_project`) |
| `opensci_engine.assembly` | local assembly layout / gross-collision check of a resolved 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_preview_delivery` / `open_detail_delivery`: open a protected delivery document in memory (extra `[protected]`) |
| `opensci_engine.client_artifacts` | `validation_artifact_upload` / `drawing_artifact_upload`: the client 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, as the tracer produced them (observed, never recomputed); `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 catalog` / `project` / `assembly` / `schema` / `validate` / `compare` | command line (`opensci-engine --help`) |

Status vocabulary: `PASS`, `WARNING`, `FAIL`, `UNKNOWN`, `NOT_APPLICABLE`, `OUT_OF_MODEL_SCOPE`.
CLI exit codes: 0 PASS/WARNING/NOT_APPLICABLE, 1 FAIL, 2 UNKNOWN, 3 OUT_OF_MODEL_SCOPE, 64 usage error,
65 a Project / catalog the local workflow cannot simulate from public data, 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.
