{
  "markdown": "# Blender Optics Simulator\n\n[![CI](https://github.com/emircbngl/blender-optics-simulator/actions/workflows/ci.yml/badge.svg)](https://github.com/emircbngl/blender-optics-simulator/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/emircbngl/blender-optics-simulator?label=release&color=brightgreen)](https://github.com/emircbngl/blender-optics-simulator/releases/latest)\n[![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)](LICENSE)\n[![Blender 4.2+](https://img.shields.io/badge/Blender-4.2%2B%20%2F%205.x-orange)](https://www.blender.org/)\n[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20778997.svg)](https://doi.org/10.5281/zenodo.20778997)\n\n**An optical bench an AI agent can build, inspect, align, and render — over MCP.**\n\n> *\"See a need, fill a need.\"* — Bigweld, *Robots* (2005)\n\n> **60-second tour** — Lay out lasers, mirrors, beamsplitters, lenses, waveplates, gratings and\n> detectors in Blender; a **live beam engine** traces them (ray + Gaussian-q ABCD + Jones/Stokes\n> polarization + wave-optics overlays), all **physics-verified** against textbook answers in CI.\n> Everything mounts on **real opto-mechanics** and renders in **Cycles**. The whole optical state is\n> exposed over a localhost **MCP bridge**, so an AI agent reads ground-truth geometry and beam data\n> and **drives the bench** — aligning, closing an AO loop, nulling fringes. **Install:** drag the\n> one-click link from the [releases page](https://github.com/emircbngl/blender-optics-simulator/releases/latest)\n> into Blender 4.2+ (it also subscribes you to updates — see *[Install & stay updated](#install--stay-updated)*).\n\nBlender becomes a physics-checked optical bench: lasers, mirrors, beamsplitters, lenses, waveplates,\npolarizers, gratings, deformable mirrors, detectors — laid out in 3-D, traced by a live beam engine,\nmounted on real opto-mechanics, and rendered in Cycles. The twist: the *entire* optical state is\nexposed as JSON over a localhost **MCP bridge**, so an AI agent (e.g. Claude) doesn't guess geometry\n— it reads every element's pose, port normals, beam path, mount limits, and wavefront error as\n**ground truth**, then drives the bench toward design intent and watches the beam update live.\n\nThis is the one place **Blender × optics × MCP/AI-agents** actually overlap. Most Blender-MCP work is\ngeometry and animation. This is a physics-grounded digital twin of a real optical table that an agent\ncan *see* and *close the loop* on.\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"docs/img/hero-bench-light.png\">\n    <img src=\"docs/img/hero-bench-dark.png\" width=\"92%\" alt=\"A Michelson interferometer on a tapped breadboard, built and traced in Blender — kinematic mounts, posts and bases on real opto-mechanics, both arms' beams glowing, rendered in Cycles\">\n  </picture>\n</p>\n\n<p align=\"center\"><em><b>A real bench, traced live.</b> Every mount, post, base and beam above is placed and computed by the add-on — then rendered in Cycles. Build it by hand in the UI, from a script, or let an AI agent drive it over MCP.</em></p>\n\n<p align=\"center\">\n  <img src=\"docs/img/agent-align.gif\" width=\"88%\" alt=\"A laser beam steered by a kinematic mirror lands well off the detector centre, then auto-alignment walks the mount until the beam is re-centred on the sensor — rendered live in Blender\">\n</p>\n\n<p align=\"center\"><em><b>Alignment, solved.</b> A kinematic mirror is knocked 2° out of alignment, throwing the beam well off the sensor centre — then one <code>optics_api.align_element()</code> call collapses the pointing residual (7.02 → 0.0008 mrad) and re-centres it. See <a href=\"examples/agent_align.py\"><code>examples/agent_align.py</code></a>, which reproduces those numbers headlessly in one command.</em></p>\n\n---\n\n## The core loop\n\nAn agent doesn't fly blind — it has the optical truth and iterates against it:\n\n```text\nget_state()  →  read every element's world pose, port positions+normals, mount DOFs,\n                 traced beam path, detector readings, wavefront error  (full JSON)\n   ↓ decide\naction()     →  align a mirror · scan OPD · place a part on a grid hole · close the AO loop\n   ↓\nbeam re-traces live  →  next iteration\n```\n\n`get_state()` returns the *whole* bench as JSON — elements, sources, detectors, beam path, bench\ngrid, cages/tubes/rails, and mechanical-limit warnings — so the agent always knows where things are\nand where the light goes. **30 functions** are exposed over MCP; the bridge whitelists only the\ncurated `optics_api` facade.\n\n---\n\n## Drive it with an AI agent (MCP)\n\n<p align=\"center\">\n  <img src=\"docs/img/ai-loop.svg\" width=\"92%\" alt=\"The agent loop: READ the bench (get_state, inspect_beam, inspect_element, diagnose, detect_phenomena, propose_corrections) → JUDGE advisory corrections by user intent (refuse / partial / accept) → ACT (set_param, align, ao_close_loop) → RE-TRACE → read again. The trace is byte-identical until you act.\">\n</p>\n\n1. In Blender: **Optics ▸ Simulation ▸ Start MCP Bridge** (exposes every public `optics_api`\n   function on `127.0.0.1:9765`).\n2. Run the bridge server in [`mcp/`](mcp/README.md) and wire it into your MCP client.\n\nThen the agent can **READ** the bench, **WRITE** changes, and **COMPOSE** subsystems:\n\n```python\nimport optics_api\n\n# COMPOSE — bootstrap a canonical bench in one call\noptics_api.build_example(\"michelson\")        # full setup + live beam\n\n# READ — the agent's numeric eyes: ground truth, not guesses\nstate = optics_api.get_state()               # poses, port normals, beam path, mounts, detectors\noptics_api.inspect_beam(\"MI_BS\")             # power, w, R(z), M², divergence, polarization, coherence here\noptics_api.inspect_element(\"MI_BS\")          # what this optic DOES + its live in/out by kind, throughput\noptics_api.beam_profile(\"MI_D\")              # Gaussian w(z), waist, aperture clipping\noptics_api.detect_phenomena()                # flags interference / off-axis-hologram conditions the trace meets\noptics_api.propose_corrections()             # advisory fixes you JUDGE (refuse / partial / accept)\n\n# WRITE — act, then the beam re-traces\noptics_api.align_all()                       # auto-tune every kinematic knob toward its target\noptics_api.set_param(\"MI_BS\", \"split_ratio\", 0.5)\noptics_api.place_on_grid(\"MI_M_fixed\", 6, 2) # slide a part to a real breadboard hole\noptics_api.scan(kind=\"STAGE\", lo=0, hi=2e-3, steps=120, element=\"MI_M_stage\")  # interferogram PNG+CSV\n\n# Adaptive optics: sense the wavefront, then close the loop\noptics_api.build_example(\"adaptive_optics\")\noptics_api.ao_measure(\"AO_WFS\")                                 # {zernike:[...], rms: 0.559} (incl. beam defocus)\noptics_api.pyramid_wfs(\"AO_WFS\")                                # same wavefront read as a SLOPE sensor\noptics_api.ao_close_loop(\"AO_WFS\", \"AO_DM\", gain=0.5, iters=15) # rms 0.559 → ~0\n\n# RENDER — photorealistic Cycles, or a turntable movie\noptics_api.bake_beams()\noptics_api.render(preset=\"final\", camera=\"HERO\", filepath=\"/tmp/michelson.png\")\n```\n\nThe full surface (each also an MCP tool): `capabilities`, `get_state`, `diagnose`,\n`propose_corrections`, `detect_phenomena`, `inspect_beam`, `inspect_element`, `beam_profile`,\n`sensor_capture`, `ao_measure`, `get_wavefront`, `pyramid_wfs`, `zonal_render`, `coupling_efficiency`,\n`check_mechanics`, `build_example`, `trace_beam`, `path_statistics`, `tag_element`, `add_component`, `swap_part`,\n`place_relative`, `set_mount`, `set_param`, `align_element`, `align_all`, `auto_align`, `tilt_null`,\n`design_telescope`, `design_4f`, `mode_match`, `scan`, `render`, `render_sequence`, `bake_beams`,\n`clear_beams`, `export_svg`, `dress_bench`, `set_grid`, `place_on_grid`, `make_cage`, `make_tube`,\n`make_rail`, `place_on_rail`, `ao_command`, `ao_close_loop`. Call `capabilities()` first — it returns a\nself-describing manifest (read [`mcp/AGENT_GUIDE.md`](mcp/AGENT_GUIDE.md) for the conventions and the\nrefuse / partial / accept judgement model).\n\nFive repeatable **`/optics-*` skills** ship in [`.claude/skills/`](.claude/skills/) (build · align ·\ninspect · correct · sensor-render) — model-invoked workflows that sequence these tools and carry the\ndisciplines (inspect-first, byte-identical, advisory-corrections-you-judge).\n\nFor headless pipelines, `optics_api` is importable directly inside Blender\n(`blender --background --python your_script.py`).\n\n### Detector path length, shutters, and custom meshes\n\n`path_statistics()` reports every source-to-detector arrival separately. It reconstructs the\nparent-indexed route and returns both its geometric length and the tracer's accumulated **phase OPL**:\n\n```python\nstats = optics_api.path_statistics()                 # all detector terminals\none = optics_api.path_statistics(\"MyDetector\")      # one detector\n```\n\nThe result deliberately says `group_delay_available: false`. The current tracer carries phase-index OPL;\nit does **not** model group index, group delay, or GDD, so the number must not be presented as an ultrafast\ntime-of-flight result. In the UI the same phase OPL/geometric range appears under **Inspect → Optical Report**.\n\nA binary shutter is available from the component library under the key `SHUTTER`:\n\n```python\ncreated = optics_api.add_component(\"SHUTTER\", location=(0, 0, 0))\noptics_api.set_param(created[\"name\"], \"shutter_open\", False)  # block\noptics_api.set_param(created[\"name\"], \"shutter_open\", True)   # transmit\n```\n\nFor a custom CAD/mesh component, import the STL/OBJ in Blender, select it, then use **Optics → Setup →\nElement**: enable *Optical element*, choose an existing *Element Type*, click **Tag Element**, then **Detect\nPorts**. Advanced mode lets you pick/edit the IN/OUT/REFLECT faces. The API equivalent is\n`tag_element(\"MyMesh\", \"SHUTTER\")`; `swap_part()` can also put custom CAD onto a generic component while\nretaining its ports and optical behaviour. Mesh appearance and optical behaviour are intentionally separate:\nnew behaviour beyond the shipped element types still requires a tracer implementation, not only a mesh.\n\n### Units: 1 Blender unit = 1 mm\n\nEvery component is built at that scale, and the whole measurement layer reads world coordinates **as\nmillimetres** — `scale_length` is not consulted by the tracer, solvers, alignment or diagnostics. It is a\nconvention, not a display preference: the same bench traced in a metre-scale scene produces a\nbyte-identical trace, because the numbers are simply reinterpreted by Blender's UI while the physics still\ncalls them millimetres.\n\nSo work with **Scene → Units → Unit Scale = 0.001**. The examples set it for you (and say so when they\nchange it). If your scene is on another unit scale, the beam panel offers **Convert Scene to Millimetres**\n(`optics_api.convert_scene_to_mm()`): it sets Unit Scale to 0.001 *and* scales your own objects by the same\nfactor, so a 2 m model stays 2 m long and optics land at true size beside it. Add-on geometry is already\nmm-authored and is left alone.\n\nThe conversion goes that way round on purpose. Rescaling the optics instead would desynchronise them from\nthe physics: a 150 mm arm would become 0.15 units and the tracer would read it as 0.15 mm — every\npropagation, path length and aperture clip wrong by 1000×, with no error raised.\n\n**If you genuinely work in another unit scale**, turn on **Scene units are authoritative**\n(`scene.optics.scene_units_authoritative`). Component arguments stay in millimetres — the API is unchanged —\nbut they are then placed at true physical size and the trace measures physical millimetres, verified by\n`tests/_verify_unit_equivalence.py`. It is off by default and that default is deliberate: Blender's factory\nUnit Scale is 1.0 and this add-on never set it, so a scene reading 1.0 is almost always one where nobody\ntouched units, not a metre-scale scene. Guessing from Unit Scale alone reinterprets every such scene and\nshrinks it by a thousand.\n\n**Opto-mechanical hardware is not supported while it is on.** Posts, breadboards and mounts are\nmillimetre-hardcoded and `check_mechanics()` compares real mesh geometry, so it would collision-check parts\na factor of a thousand apart. `dress_bench()` and `check_mechanics()` refuse rather than report a wrong\nanswer.\n\nThe add-on does not guess. Add a component in a scene on another unit scale and it is still placed at its\ntrue millimetre size, with a warning naming the factor — `add_component()` returns a `warning` key and the\nUI reports it. Supporting non-mm scenes properly means making the measurement layer unit-aware; that is\ntracked separately.\n\n---\n\n## What you can do\n\n- **Design** a real bench — optical element types on real opto-mechanics: tapped-hole\n  breadboards (metric 25 mm / imperial 1″), **9 real mount presets** (KM100/KS1/POLARIS-class\n  kinematics, GM100 gimbal, RSP1 rotation, TRF90 flip, VC1 V-clamp — real DOF ranges, detents and\n  clamping), Ø12.7 mm posts + post-holders, **16/30/60 mm cage** systems, **SM05/SM1/SM2 lens\n  tubes**, and **RLA dovetail + X95 structural rails** with carriers — all original GPL-clean\n  procedural geometry to true functional dimensions, structurally checked in CI.\n- **Simulate** the light — a live beam tracer with analytic overlays: polarization (Jones/Stokes, plus a\n  **per-pixel DoFP polarization camera**), interference & fringe visibility, Gaussian-beam propagation\n  (ABCD `w(z)`, ROC, Gouy, M²), **modal *and* pyramid** wavefront sensing + closed-loop **adaptive\n  optics**, wavelength-selective materials (interference/colored-glass filters, **soft-edge dichroics**),\n  nonlinear conversion (SHG/SPDC), and acousto-optic Bragg deflection.\n- **Let an AI drive it** — the agent READS numeric ground truth (`inspect_beam` / `inspect_element` /\n  `get_state`), is told the **phenomena** the bench meets (`detect_phenomena`: interference, off-axis\n  hologram) and the **advisory corrections** it should weigh against your intent (`propose_corrections`:\n  refuse / partial / accept), and follows repeatable `/optics-*` skills — never guessing, every formula\n  oracle-verified.\n- **Align** — kinematic auto-alignment that drives only the mount knobs to < 1 mrad and reports\n  *\"move the post\"* when a target is out of reach; a geometric validator flags collisions and posts\n  in the beam.\n- **Render** — bake beams to glowing emission tubes, one-click EEVEE preview / Cycles final with\n  glass materials + studio lighting, camera presets, transparent-PNG figures, turntable movies, and\n  publication-ready SVG schematics.\n\n### 26 one-click example scenes\n\nEach is a single `build_example(kind)` — built from portable, mesh-free generic components, fully\nagent-drivable:\n\n| Scene | What it shows |\n|---|---|\n| **Mach-Zehnder** | unbiased beamsplitter + dual-arm phase exploration |\n| **Michelson** | translation-stage OPD fringes under single-knob control |\n| **Hong-Ou-Mandel** | two-photon interference dip (analytic quantum readout) |\n| **Bell / entanglement** | BBO pair source + H/V analysis, CHSH \\|S\\|=2√2 |\n| **Adaptive optics** | aberrator + Hartmann sensor + deformable mirror, loop → ~0 RMS |\n| **Newton's rings** | curved vs flat wavefront → live concentric rings |\n| **Periscope** | two 45° fold mirrors raise the beam to a second deck (RS99-style) |\n| **Cage system** | 30 mm cage relay: fiber → collimator → polarizer → PBS, 50/50 split |\n| **Lens-tube system** | SM1-barrel 4f relay → C-mount camera |\n| **Rail system** | Galilean beam expander on a dovetail rail |\n| **Hybrid system** | cage launcher + post mirror + rail analyzer on one board |\n| **Microscope** | infinity-corrected Köhler train, `f_obj = f_tube/M` |\n| **DHM** | vertical off-axis Mach-Zehnder recording a hologram (amplitude + phase) |\n| **AOM** | TeO₂ Bragg cell, 0th + frequency-shifted +1 order, θ = λ·f_a/v_s |\n| **Surface figure** | oblique beam off a figured reflector → dense zonal wavefront a WFS reads |\n| **Die** | a die face (the 5-pip quincunx) read as a recognizable zonal wavefront |\n\n…plus ten more: **prism** (dispersing-prism spectrometer, white light fanned by n(λ)), **beam_router**\n(rhomboid parallel-displace + tilt-invariant penta steering), **beam_profiler** (knife-edge erf scan),\n**back_reflection** (a window's parasitic Fresnel retro-ghost), **green_doubler** (1064→532 nm SHG),\n**spdc_source** (degenerate 810 nm signal+idler pairs), **quad_tracker** (4-quadrant position error),\n**circulator** (non-reciprocal 3-port fiber router), and the **surface_figure_native /\nsurface_figure_diverging** WFS variants.\n\n---\n\n## Gallery\n\nEverything below is produced by the add-on itself — a viewport beam trace baked to emission tubes,\nthe Scan + Plot operator, the Detector Fringe Image, and the live sensor window exported to PNG.\n\n<p align=\"center\">\n  <img src=\"docs/img/feature-board.png\" width=\"92%\" alt=\"New in v0.10.0: the WFS reads the beam's own curvature defocus; a pyramid WFS slope read; a soft-edge dichroic (R+T=1); a die face read as a zonal wavefront\">\n</p>\n\n### New — catalog-true cage hardware + a structural-support gate\n\n<p align=\"center\">\n  <img src=\"docs/img/cage-er6-train.png\" width=\"49%\" alt=\"A 30 mm cage train on catalog rods: four ER6 rods end just past the outer plates, each optic gripped by its plate bore, the assembly post-mounted through one plate\">\n  <img src=\"docs/img/cage-single-plate.png\" width=\"49%\" alt=\"A single-member cage: one bored cage plate on its post, no rods — exactly what a lone plate looks like on a real bench\">\n</p>\n\nCage assemblies now build from **real catalog parts**: rods snap to the vendors' fixed\nER/SR lengths (a 130 mm train picks the **ER6 · 152.4 mm** rod and shows the true symmetric\noverhang; `cage_info()` reports the part), a single-member cage stands **rodless** on its\nplate, the assembly is post-mounted through one plate's bottom tap, plate bores close onto\neach optic's actual mesh, and lens tubes carry real retaining rings. Behind it sits a new\n**structural-support gate**: `support_scan()` measures the mesh-level gap between every\noptic and the hardware meant to hold it, and `validate()`/`diagnose()` flag anything\nunsupported — so floating hardware cannot ship silently, in any scene.\n\n<p align=\"center\">\n  <img src=\"docs/img/base-tab-macro.png\" width=\"62%\" alt=\"When a post axis misses the breadboard's hole grid, the base grows a slotted clamping ear that reaches the nearest hole — with its screw seated in the slot\">\n</p>\n\n### New in v0.25.0 — expanded opto-mechanics: 9 mount presets, X95 rails, structural QA in CI\n\n<p align=\"center\">\n  <img src=\"docs/img/optomech-showcase.png\" width=\"49%\" alt=\"New in v0.25.0: an 11-station traced bench on the expanded opto-mechanics — V-clamped laser, rotation-mounted waveplate, flip-mounted filter, caged lens, rail lens, kinematic fold, gimbal fold, camera\">\n  <img src=\"docs/img/optomech-gimbal-flip.png\" width=\"49%\" alt=\"New in v0.25.0: close-up of the GM100-class gimbal fold and the TRF90 flip mount — yoke, pivot studs and hinge seat correctly for any beam direction\">\n</p>\n\n**4 new mount presets** — RSP1 rotation, GM100 gimbal, TRF90 flip with real 0°/90° detent\nsemantics, VC1 V-clamp for cylindrical bodies — plus the **X95 structural rail** family next to\nthe RLA dovetail. The whole opto-mechanical catalog was rebuilt to true functional geometry:\nbases, hinges and clamps seat correctly for **any** beam direction (including folded and vertical\npaths), preset apertures use the vendors' real dimensions, and every optic sits in genuine contact\nwith its hardware. A render-free structural check now runs in CI on every push\n(`tests/test_mesh_health.py`: mesh integrity per element, part-contact analysis per mount\nassembly), so the catalog cannot silently regress.\n\n### New in v0.24.0 — laser speckle + the coffee-cup caustic (phenomena closed out)\n\n<p align=\"center\">\n  <img src=\"docs/img/speckle-demo.png\" width=\"49%\" alt=\"New in v0.24.0: fully-developed laser speckle — the grainy pattern from a random-phase diffuser, with its intensity histogram matching the negative-exponential PDF\">\n  <img src=\"docs/img/caustic-demo.png\" width=\"49%\" alt=\"New in v0.24.0: the coffee-cup caustic — parallel rays reflecting off a circular mirror pile onto a nephroid whose cusp sits at the mirror focus R/2\">\n</p>\n\nTwo off-trace phenomenon producers, each derived from first principles and checked against its exact\ntextbook result. **`speckle_pattern()`** — a random-phase diffuser propagated to a screen gives\nfully-developed **laser speckle**: contrast `σ_I/⟨I⟩ → 1`, a negative-exponential intensity PDF, mean grain\n`~ λz/D`, and `1/√N` suppression when you average `N` frames (contrast + averaging law *physics_verify*\n`ok=true`). **`caustic_pattern()`** — parallel rays reflecting off a circular mirror's concave wall pile up\non a **nephroid** (the coffee-cup caustic), the ray-density lighting up the analytic envelope ~20× with its\ncusp exactly at the mirror focus `R/2` (reflection geometry + cusp *physics_verify* `ok=true`). This closes\nthe phenomenon-emergence catalog (hologram · interferogram · Fabry–Pérot · Talbot · Newton's-rings · speckle\n· caustic). Also new: **`diagnose()` warns when an optic is knocked off the beam** (`optic_bypassed`) — a mid-path\nlens/mirror/BS the beam no longer reaches now raises a WARN with a re-centre fix, closing a gap a render surfaced.\n\n### New in v0.23.0 — opto-mechanical hardware rides its optic (rigid assemblies)\n\n<p align=\"center\">\n  <img src=\"docs/img/rigid-mount-fix.png\" width=\"92%\" alt=\"New in v0.23.0: before/after of the Newton's-rings bench — grabbing the lens steps its post, holder and mount aside with it as one rigid body; all 7 dressed hardware objects move by exactly the lens delta, and the live trace stays byte-identical\">\n</p>\n\nDressed hardware (post, holder, mount) used to be built as **standalone** objects at fixed world\ncoordinates, so grabbing an optic — e.g. a lens in the `newton_rings` bench — left its mount behind.\nNow each cluster's hardware is **rigid-parented** to its optic via `matrix_parent_inverse`, so the\nwhole assembly moves as one body: the parenting causes **no visible jump**, leaves the optic's\n`matrix_world` untouched, and the **live trace stays byte-identical** (regression 392/392). Also new:\n`inspect_all()` (a per-element dashboard) and `export_report()` (a self-contained HTML spec-sheet of\nthe whole bench), the always-visible in-add-on **update button**, four more glasses + CLBO/AgGaS2 +\nthe **As2S3/AgCl/ZnS** infrared materials (sourced, with a new `docs/DATASOURCES.md` provenance doc),\nand a distinct translation-stage mount.\n\n### New in v0.22.0 — biaxial nonlinear crystals (KTP + LBO) + Newton's-rings 2D\n\n<p align=\"center\">\n  <img src=\"docs/img/biaxial-crystals-demo.png\" width=\"92%\" alt=\"New in v0.22.0: KTP and LBO XY-plane SHG phase-match tuning curves passing through the textbook 1064-to-532 cuts (KTP Type-II 23.5 deg, LBO Type-I 11.6 deg), plus their three principal indices\">\n</p>\n\nThe Sellmeier-derived SHG phase matching covered only **uniaxial** crystals; this adds the two workhorse\n**biaxial** doublers, **KTP** and **LBO**, with all three principal-axis Sellmeier sourced from\nrefractiveindex.info. In the **XY principal plane** the biaxial problem reduces to an effective-uniaxial one,\nand from the sourced coefficients the solver reproduces the textbook Nd:YAG green-doubler cuts —\n**KTP Type-II φ=23.58°** (lit 23.5°), **LBO Type-I φ=11.76°** (lit 11.6°) — plus their spatial walk-off (KTP\n~4 mrad, LBO ~7 mrad). Off-trace calculators; the live trace stays byte-identical. *(The in-plane index order\nwas caught and corrected via `physics_verify` before coding — the polarization is perpendicular to the\nwavevector, so `1/n² = sin²φ/n_x² + cos²φ/n_y²`.)*\n\nAlso new: **`newton_rings`** produces the 2-D Newton's-rings reflected pattern of a plano-convex surface on a\nflat — `I(r)=sin²(πr²/λR)`, a central dark spot and dark rings falling exactly on `r_m=√(mλR)`\n(`docs/img/newton-rings-demo.png`).\n\n### New in v0.21.0 — birefringence: o/e double refraction + a χ² coupled-wave solver\n\n<p align=\"center\">\n  <img src=\"docs/img/birefringence-demo.png\" width=\"92%\" alt=\"New in v0.21.0: true ordinary/extraordinary double refraction — one beam enters a calcite crystal and two parallel beams (ordinary + extraordinary) leave, separated by L*tan(rho), the textbook double image\">\n</p>\n\nThe two biggest architectural items on the backlog — both **in the live tracer**, yet **byte-identical** (each\nbehind a per-element opt-in flag, default off). **`oe_split`** gives a uniaxial crystal **true ordinary/\nextraordinary spatial double refraction**: one beam in, two orthogonally-polarized beams out, the extraordinary\none walked off by `L·tan(ρ)` (the calcite double image). **`use_chi2_solver`** derives a nonlinear crystal's\nSHG efficiency from the **full Manley-Rowe coupled-wave ODE** — pump depletion + phase mismatch, RK4-integrated,\nreproducing `tanh²(√η_lin)` at phase match and `η_lin·sinc²` undepleted (both verified to ~1e-12), with the\nwalk-off derived from the index ellipsoid. The tracer already branches rays (beamsplitter, Wollaston) and emits\nwalk-off SHG children, so each is the same split-two pattern — not a new engine.\n\n<p align=\"center\">\n  <img src=\"docs/img/chi2-solver-demo.png\" width=\"92%\" alt=\"New in v0.21.0: the chi(2) tensor solver — SHG conversion saturating with pump depletion, Manley-Rowe energy conservation (pump + harmonic = input), and the depleted vs undepleted phase-mismatch tuning curve\">\n</p>\n\n### New in v0.20.0 — wave optics: aberrated PSF, phase retrieval & cavity modes\n\n<p align=\"center\">\n  <img src=\"docs/img/aberrated-psf-demo.png\" width=\"92%\" alt=\"New in v0.20.0: the diffraction PSF aberrated by single Zernike modes — defocus, astigmatism, coma, spherical — each 0.10 waves RMS; the Strehl collapses to ~0.674 (Maréchal) for all of them while the PSF shape is the mode's fingerprint\">\n</p>\n\n**`aberrated_psf`** — the *forward* problem (wavefront → image). `wave_psf` could only add **Z4 defocus**; this\naberrates the diffraction PSF with **any Zernike mode** (defocus, astigmatism, coma, trefoil, spherical) at a\nchosen RMS, or a full Noll-indexed wavefront vector. The **Strehl** ratio collapses the same way for every\nmode — it depends only on the RMS wavefront error (the **Maréchal** approximation exp(−(2π·rms)²): 0.05 waves →\n0.906, 0.10 waves → 0.674) — but the PSF *morphology* is the mode's fingerprint: defocus blurs symmetrically,\nastigmatism elongates, coma flares one-sided, spherical haloes.\n\n<p align=\"center\">\n  <img src=\"docs/img/fienup-phase-retrieval-demo.png\" width=\"92%\" alt=\"New in v0.20.0: Fienup HIO phase retrieval — reconstruct a hidden object from its diffraction intensity |FFT|^2 plus a support mask; HIO escapes the error-reduction stagnation, recovering the object to correlation 0.996\">\n</p>\n\n**`fienup_phase_retrieval`** — the *inverse* problem (the genuine **phase problem**). A detector records only the\ndiffraction **intensity** `|FFT(object)|²`; the phase is lost. Given that magnitude plus a real-space **support\nmask**, Fienup's **Hybrid-Input-Output** algorithm reconstructs the hidden object (correlation 0.996, up to the\ninherent translation + twin ambiguity). Where v0.19.0's Gerchberg-Saxton knows the amplitude in *both* planes\n(CGH design), here the object is **unknown** — only its support is; the HIO feedback escapes the stagnation pure\nerror-reduction falls into.\n\n<p align=\"center\">\n  <img src=\"docs/img/tem-modes-demo.png\" width=\"92%\" alt=\"New in v0.20.0: laser cavity transverse modes — Hermite-Gaussian TEM_mn lobe patterns and Laguerre-Gaussian donut modes with their exp(i*l*phi) phase vortices carrying orbital angular momentum\">\n</p>\n\n**`tem_mode`** — the laser cavity **transverse eigenmodes** a real resonator emits. `family=\"HG\"` gives the\n**Hermite-Gaussian TEM_mn** (rectangular, (m+1)(n+1) lobes, an orthonormal set); `family=\"LG\"` gives the\n**Laguerre-Gaussian** donut `LG_{p,l}` with an on-axis null and an `exp(i·l·φ)` **phase vortex** carrying\n**orbital angular momentum** `l·ħ`. All three additions are off-trace; the geometric trace stays byte-identical.\n*(This release also fixes a latent `NameError` that would have crashed the `gerchberg_saxton` / `spatial_filter`\n/ `propagate_chain` MCP tools when called by an agent, and adds an end-to-end guard so it can't recur.)*\n\n### New in v0.19.0 — Fourier optics: phase retrieval + spatial filtering\n\nA Fourier-optics pair on the verified FFT: **`gerchberg_saxton`** (the iterative Fourier-transform algorithm)\n*designs* a source-plane phase mask — a computer-generated hologram — whose far-field matches a target, with\nthe monotone-error convergence guarantee; **`spatial_filter`** (the Abbe-Porter experiment) *applies* a\nFourier-plane mask (lowpass / highpass / Zernike phase contrast that makes a pure-phase object visible). Both\noff-trace; the geometric trace stays byte-identical.\n\n### New in v0.18.0 — multi-plane field-propagation chain (`propagate_chain`)\n\n`propagate_field` runs the angular-spectrum propagator once; the new **`propagate_chain`** marches a complex\nfield through a **sequence** of planes — a POPPY-style multi-plane OpticalSystem built from `prop` / `aperture`\n/ `lens` steps (e.g. `aperture→lens(f)→prop(f)` focuses at z=f). Closes the optics-textbook catalog's principal\nremaining gap (single plane→plane is now a chainable auto-pipeline). Validated against propagator additivity,\nfocus-at-f, and the free Gaussian `w(z)`. Off-trace; the geometric trace stays byte-identical. *(Near-field\nlayer — a tight focus or Fraunhofer far field is better via direct FFT.)*\n\n### New in v0.17.0 — dichroic AOI edge + Sellmeier transparency window\n\nTwo physical-honesty refinements: a thin-film **dichroic mirror's edge now blue-shifts with the angle of\nincidence** (a 45° dichroic sits ~8% to the blue of its normal-incidence spec — the fluorescence-microscopy\ngotcha), consistent with the interference filters; and **`sellmeier_in_range`** flags when a refractive-index\nquery falls outside a glass's published fit window (an extrapolation, not a measured value). Both keep the\ngeometric trace byte-identical.\n\n### New in v0.16.0 — Fabry-Pérot + Talbot emergence (phenomenon catalog complete)\n\n`produce_phenomenon` now covers **all four** phenomena: `detect_phenomena` also flags a **Fabry-Pérot cavity**\nand a **Talbot grating**, and `produce_phenomenon` emerges the **Airy transmission resonance** (finesse, FSR,\ncontrast) and the **Talbot self-imaging** (`z_T = 2 d²/λ`) — behind the same advisory accept-gate. Validation\nnow **163 oracle checks**.\n\n### New in v0.15.0 — phenomenon emergence (conditions met → the phenomenon is produced)\n\n`detect_phenomena` flags when an optical phenomenon's conditions are met; the new **`produce_phenomenon`** now\nactually **produces** it — synthesizing the two-beam **interferogram** and **recording + reconstructing an\noff-axis hologram** (carrier `Λ = λ/(2 sin θ/2)` → 3.63 µm, the object beam's angle recovered from the carrier\npeak). It stays **advisory + intent-judged** like `propose_corrections`: a dry-run with an intent caveat first\n(*\"the camera may be a power meter, not a hologram plate\"*), then `accept=True` emerges the pattern — never\nsilently. Off-trace; each result self-checks against a textbook oracle.\n\n### New in v0.14.0 — uniaxial crystal optics + Mueller calculus\n\nClosed-form **anisotropic-ray** numbers on the existing ordinary/extraordinary index catalog: birefringent\n**walk-off angle** (calcite at 45° → 6.224°), the **index ellipsoid** n_e(θ), true zero-order **waveplate\nthickness** (quartz QWP = 16.19 µm), and the **Type-I SHG phase-matching angle** (BBO 1064→532 nm → 22.78°).\nPlus a **4×4 Mueller calculus** (`M_mueller_polarizer`/`M_mueller_retarder`/`stokes_through`) for\npartially-polarized light, validated against py-pol / SymPy / Malus, and end-to-end CI coverage of the\nwaveplate crystal-birefringence dispersion branch. Validation now **152 oracle checks**.\n\n### New in v0.13.0 — diffraction & focusing examples + a verified FDTD bridge\n\nSingle/double/N-slit **Fraunhofer diffraction**, the **Talbot self-imaging** carpet, and a Fresnel\ndiffraction & focusing batch (circular-aperture zones, knife-edge, Fresnel **zone-plate**, thin-lens focus,\nFabry–Pérot Airy minimum, **Newton's rings**) — each reproduced by the field engine and pinned to its\ntextbook closed form (validation now **136 oracle checks**). And the **FDTD bridge's Meep API is now verified**\nagainst a real Meep 1.33.0 (12/12 vs exact analytic oracles; `tools/verify_fdtd_meep.py`), with three real\nsetup bugs fixed. Core trace byte-identical to v0.12.0.\n\n### New in v0.12.0 — the physical-optics field engine (opt-in, off-trace)\n\nA complete sampled-field layer on top of the geometric + Gaussian-q core. Every panel is computed **on\ndemand** and **never touches the live trace** (the byte-identical regression is preserved); every formula is\nmachine-verified against the physicist Docker oracle (the validation suite now runs **136 checks**).\n\n| | |\n|:---:|:---:|\n| ![soliton vs disperser](docs/img/field-soliton.gif) | ![star dissolving into the seeing disk](docs/img/field-seeing.gif) |\n| **split-step NLSE** — the fundamental soliton holds its shape while an ordinary pulse disperses (`propagate_pulse`) | **imaging through turbulence** — a star dissolves into the seeing disk as D/r₀ climbs (`propagate_turbulent`) |\n\n<p align=\"center\"><b>▶ <a href=\"docs/img/field-reel.mp4\">Watch the field-engine reel</a></b> — diffraction · propagation · turbulence · solitons, in 18 seconds. <em>All off-trace; the live ray-trace stays byte-identical.</em></p>\n\n| Diffraction PSF (Fourier optics) | Free-space field propagation | Imaging through turbulence |\n|:---:|:---:|:---:|\n| ![PSF](docs/img/wave-psf-demo.png) | ![field propagation](docs/img/field-propagation-demo.png) | ![seeing](docs/img/turbulence-psf-demo.png) |\n| `wave_psf` — PSF = \\|FFT(pupil·e^{i2πW})\\|² → Airy 1.22 λF#, Strehl, MTF, encircled energy | `propagate_field` — angular-spectrum: a Gaussian recovers w(z), reversible (= digital-hologram back-prop) | `propagate_turbulent` — a plane wave through 5 Kolmogorov screens; the long-exposure PSF blurs to the seeing disk |\n\n| Atmospheric turbulence screen | Nonlinear pulse (split-step NLSE) | Monte-Carlo tissue transport |\n|:---:|:---:|:---:|\n| ![turbulence](docs/img/turbulence-screen-demo.png) | ![NLSE](docs/img/nlse-pulse-demo.png) | ![MCML](docs/img/mc-tissue-demo.png) |\n| `turbulence_screen` — dense Kolmogorov phase screen; D(r) = 6.88 (r/r₀)^{5/3} | `propagate_pulse` — the fundamental soliton stays shape-invariant; SPM broadens the spectrum | `monte_carlo_tissue` — MCML photon transport; R+T+A = 1, the diffusion penetration depth |\n\n*Plus `fdtd_derive_property` (orchestrates Meep/Tidy3D for a grating/coating/metasurface, closed-form fallback\nwhen absent), `tolerance_scan` (pose-only Monte-Carlo alignment yield), and the `/optics-scope` skill that\nanswers \"can it simulate X?\" honestly — by tier, with the verified case or the external tool to use.*\n\n<p align=\"center\"><b>▶ <a href=\"docs/img/showcase.mp4\">Watch the showcase film</a></b> — build → simulate → align → wavefront → an agent driving it, end to end.</p>\n\n<p align=\"center\">\n  <img src=\"docs/img/michelson-orbit.gif\" width=\"72%\" alt=\"A dressed Michelson interferometer on an optical breadboard, orbited in a Cycles turntable, the laser beams glowing along both arms\">\n</p>\n\n<p align=\"center\"><em>A dressed <b>Michelson</b> on a tapped breadboard — kinematic mounts, posts, glowing beams — turntabled in Cycles (<code>render_sequence</code>).</em></p>\n\n| Interferogram | 2-D fringes (tilted) | Malus' law |\n|:---:|:---:|:---:|\n| ![interferogram](docs/img/interferogram.png) | ![2-D fringes](docs/img/fringe-2d.png) | ![Malus curve](docs/img/malus.png) |\n| Michelson intensity vs. optical path difference | straight fringes with the Gaussian-beam apodization envelope | polarizer transmission ∝ cos²θ |\n\n| Newton's rings | Aberrated wavefront | Corrected wavefront |\n|:---:|:---:|:---:|\n| ![Newton's rings](docs/img/newton-rings.png) | ![aberrated wavefront](docs/img/wavefront-aberrated.png) | ![flattened wavefront](docs/img/wavefront-flattened.png) |\n| a lens vs. a flat reference → concentric rings, r_m = √(mλR) | AO sensor: injected defocus + astigmatism + coma (RMS 0.56 λ) | after the closed loop → flat (RMS 0.00 λ) |\n\n**Wavefront sensing — modal *and* zonal, honest about the difference.** The same surface figure reads\nthree ways through three beams; the dense zonal map keeps high-spatial-frequency relief the 15-mode\nmodal fit smooths away; recognizable objects (a chess knight, a die) make the map legible.\n\n| One figure, three beams | Modal (low-pass) vs zonal (dense) | Recognizable objects |\n|:---:|:---:|:---:|\n| ![surface figure, 3 beams](docs/img/surface-figure-3beams.png) | ![zonal wavefront](docs/img/zonal-wavefront.png) | ![object wavefronts](docs/img/object-wavefronts.png) |\n| bare / collimated / diverging — a finite sensor reads only its aperture, the sim produces the difference | the 15-Zernike fit vs the raw px×px field, up to the grid Nyquist | a knight + a die read as zonal wavefronts (`build_example('die')`) |\n\n<p align=\"center\">\n  <img src=\"docs/img/dressed-bench.png\" width=\"62%\" alt=\"A Michelson interferometer on a tapped-hole breadboard: kinematic mirror mounts, a cube beamsplitter mount, posts in post-holders, all at one beam height\">\n</p>\n\n*One-click **Dress Bench** seats every optic at one **beam height** on a real **tapped-hole\nbreadboard** (metric 25 mm or imperial 1″) with **mount-type-correct hardware** — kinematic mirror\nmounts (KM100/KS1-style), a cube beamsplitter mount, threaded lens cells, rotation, gimbal, flip\nand V-clamp mounts — on Ø12.7 mm posts in post-holders. Decoration only (never traced), so a Cycles render reads like a real\ntable. It's also a data model: `get_state()` reports the grid, beam height, vertical post chain, and\nwhich hole/system each optic uses — so an agent knows exactly where things go.*\n\n| Cage system (16/30/60 mm) | Lens tube (SM05/SM1/SM2) | Dovetail rail + carriers |\n|:---:|:---:|:---:|\n| ![cage](docs/img/sys-cage.png) | ![lens tube](docs/img/sys-tube.png) | ![rail](docs/img/sys-rail.png) |\n| collinear optics share 4 rods + one post (`make_cage`) | an in-line stack shares one barrel (`make_tube`) | carriers slide along one track (`make_rail` / `place_on_rail`; `family=\"X95\"` for the structural rail) |\n\n*Every mounting system is original GPL-clean procedural geometry built to real functional dimensions\n(Thorlabs SM threads, ER cage rods, RLA rail) — vendor CAD is never bundled. Grouping optics into a\ncage/tube/rail never moves them, so the beam path is byte-identical.*\n\n<p align=\"center\">\n  <img src=\"docs/img/microscope.png\" width=\"80%\" alt=\"An infinity-corrected microscope optical train on a breadboard: lamp, condenser, sample, a colour-ringed objective, tube lens, and camera, with the beam traced through\">\n</p>\n\n*A complete **infinity-corrected microscope**: a Köhler **lamp + condenser** image the illumination\nonto the **sample**, the **objective** collimates into the infinity space, and a **tube lens** forms\nthe image at the **camera**. The objective is a real element — `f_obj = f_tube / M` (**oracle-verified**:\n10× on a 200 mm tube lens → 20 mm), with a numerical aperture, working distance, and DIN colour ring.\nEvery lens applies true Gaussian-beam (ABCD) focusing, so the beam genuinely converges.*\n\n<p align=\"center\">\n  <img src=\"docs/img/aom.png\" width=\"62%\" alt=\"An acousto-optic modulator: a clear crystal with a gold piezo transducer bonded underneath, splitting an input beam into an undiffracted 0th order and a deflected +1 order\">\n</p>\n\n*An **acousto-optic modulator / Bragg cell** — a clear TeO₂ crystal with a **piezo transducer**\nlaunching a travelling acoustic grating. The beam splits into the undiffracted **0th order** and a\nfrequency-shifted **+1 order** deflected by `θ = λ·f_a/v_s` — the **oracle-verified** Bragg relation\n(shear-mode, `v_s ≈ 650 m/s`, `f_a = 200 MHz` → a visible ≈ 11°). Deflection angle, grating period,\nand frequency shift are all physics-checked in the sandbox.*\n\n**Components & physics.** Real ray-bending elements on real opto-mechanics — every behaviour from\nelement properties, every formula oracle-verified.\n\n| Dispersing prism | Nonlinear crystal | Ruled / holographic / echelle gratings |\n|:---:|:---:|:---:|\n| ![prism dispersion](docs/img/prism-dispersion.png) | ![nonlinear crystal](docs/img/nonlinear-crystal.png) | ![grating profiles](docs/img/grating-profiles.png) |\n| vectorial Snell + real Sellmeier glasses fan white light | SHG / SPDC / OPO with phase-matching (BBO/KTP/LBO/PPLN) | ruled, holographic, echelle, and PPLN groove meshes |\n\n| Back-reflection ghost | Coating pickoff | Closing iris |\n|:---:|:---:|:---:|\n| ![ghost beam](docs/img/ghost-beam.png) | ![coating pickoff](docs/img/coating-pickoff.png) | ![iris](docs/img/iris-closing.png) |\n| opt-in Fresnel ghosts at transmissive faces; an isolator clears the `back_reflection` flag | a paintable reflectance pickoff + neutral absorber, R + T + A = 1 | a real multi-leaf iris stops down the beam (a data model, byte-identical) |\n\n---\n\n## Methods — physical & digital\n\nTwo halves: the **physics** the simulator models, and the **engineering** that keeps it honest.\n\n**Physical methods** (the optics, all in a dependency-free `physics.py`):\n\n- **Geometric chief-ray tracing** through sequential elements — Snell refraction, vectorial Fresnel, multi-bounce splits.\n- **Polarization** — Jones vectors/matrices, Stokes / DOP, Malus, exact 3-D Fresnel s/p phase, and a per-pixel DoFP polarization camera.\n- **Gaussian beams** — the complex *q*-parameter through ABCD systems: `w(z)`, `R(z)`, Gouy phase, beam quality `M²`, mode-matching.\n- **Wavefront sensing** — modal 15-Noll-Zernike *and* pyramid (slope) reads, closed-loop adaptive optics, and dense zonal surface-figure maps (`W = 2·cos²θ·Δdepth`).\n- **Wavelength** — Sellmeier dispersion `n(λ)`, soft-edge dichroics (`R(λ) + T(λ) = 1`), interference / colored-glass filters, the grating equation.\n- **Interference & phenomena** — coherent recombination + fringe visibility, off-axis-hologram carrier spacing `Λ = λ/(2 sin θ/2)`, nonlinear χ² conversion (SHG / SPDC), acousto-optic Bragg deflection.\n\n**Digital methods** (the engineering that makes it trustworthy):\n\n- **Property-driven & byte-identical** — every behaviour is *element properties × `matrix_world`*; the mesh is cosmetic and the trace is deterministic until you act (the architecture below).\n- **Oracle-verified** — every core formula is machine-checked against an external symbolic + numeric oracle (units, identities, limits, known-input → known-answer) before it ships: **verified**, not \"dimensionally plausible.\"\n- **A 357-check regression harness** runs headless in Blender on every push (CI-green), and opt-in features stay trace-byte-identical.\n- **AI-drivable over MCP** — the agent loop *read → judge → act → re-trace*, advisory corrections you judge (refuse / partial / accept), and five `/optics-*` skills.\n- **Reproducible figures** — the analysis figures are composed by `tests/_plot_*.py` from dumped trace data through a shared [figure schema](docs/FIGURE_STYLE.md) (`figstyle`) that **guarantees the colorbar and captions never overlap or clip**, and is re-themeable without touching the layout.\n\n---\n\n## Physics & scope (honest)\n\n<p align=\"center\">\n  <img src=\"docs/img/architecture.svg\" width=\"92%\" alt=\"Architecture: each ELEMENT carries properties + a matrix_world pose (the mesh is cosmetic); the TRACER reads properties × matrix_world per element; PHYSICS adds analytic overlays (Jones/Stokes, Gaussian q-ABCD, Zernike WFS+AO, vectorial Fresnel); the READOUTS are fringes, wavefront, power, Stokes, phenomena, renders. Every core formula is machine-verified against the physicist Docker oracle.\">\n</p>\n\nThe **live trace** is a **single-ray (chief-ray) geometric tracer** (`tracer.py`) with **analytic physics\noverlays** — Jones/Stokes polarization, Gaussian-beam ABCD propagation, and analytic interference / fringe\nmodels, all in a dependency-free `physics.py`. The live trace itself is **not** a wave-optics solver — it\nstays instant and **byte-identical**. On top of it sits an **opt-in, on-demand physical-optics field engine**\n(new in v0.12.0) — the real diffraction PSF, sampled-field propagation, turbulence, nonlinear pulses,\nMonte-Carlo transport, FDTD orchestration — that you call explicitly and that **never touches the live trace**,\nso the byte-identical regression is preserved. It is **not** a lens-design optimizer.\n\nBut what it *does* model, it models honestly. The **core formulas below are checked against an\nexternal symbolic + numerical oracle** (the `physicist` verifier: units, symbolic identities, limits,\nand known-input → known-answer) — so they are *verified*, not merely \"dimensionally plausible.\" The\nbroader element-variant catalog is physically modeled but **not each independently oracle-checked** —\nsee [docs/OPTICAL_ELEMENTS.md](docs/OPTICAL_ELEMENTS.md) for per-element provenance.\n\n| Layer | Models | Validated against |\n|-------|--------|-------------------|\n| **Polarization** | Jones vectors/matrices: source state, polarizer (Malus), waveplate (HWP/QWP + fast axis), PBS (s/p); Stokes/**DOP**; a **per-pixel DoFP polarization camera** (0/45/90/135° → single-shot S0/S1/S2 + DoLP/AoLP) | Malus cos²θ, QWP→circular, PBS 50/50, DoFP S₀/S₁/S₂ ≡ Stokes |\n| **Interference** | coherent recombination (OPL→phase), coherence envelope from linewidth, fringe **visibility**; complementary Mach-Zehnder outputs; **phenomena detection** (interference / off-axis hologram conditions, advisory) | V=1 at zero OPD, energy conservation, carrier Λ=λ/(2 sin θ/2) |\n| **Wavelength** | **soft-edge dichroic** routing (logistic R(λ), T=1−R), filters (LP/SP/BP/ND), grating equation `mλ = d·Δsinθ`, **dispersion** n(λ) (Sellmeier) | grating angle, d-line n(587.6)=1.5168, R+T=1 |\n| **Gaussian beam** | complex q-parameter through free space + lenses (ABCD), spot size w(z) with beam-quality **M²**, aperture clipping; wavefront curvature R(z) + Gouy phase rendered into the fringes | focal spot λf/πw₀, Gouy→±90°, θ=M²λ/πw₀ |\n| **Wavefront / adaptive optics** | modal Zernike wavefront error per beam (Noll j=1..15), aberrator / deformable mirror / wavefront sensor; the WFS read folds in the beam's **own curvature defocus**; closed-loop modal correction; a **pyramid** (slope) read of the same wavefront | Zernike orthonormality + RMS = √(Σcⱼ²), loop RMS 0.56→0, defocus a₄=w²/4√3Rλ, pyramid dZ4/dx=4√3·x |\n| **Newton's rings** | a lensed arm vs a flat reference → wavefront-curvature (1/R) ring pattern | dark-ring radius r_m = √(mλR) |\n| **Advanced** | **exact vectorial (3-D)** Fresnel at arbitrary mirror tilt (true s/p phase, linear→elliptical), white-light fringe packets, detector **camera model** (shot/read noise, saturation), one-way **isolators**, **Fabry-Pérot** cavities (Airy/finesse/FSR) | Fresnel Brewster/normal, Airy comb |\n| **Nonlinear / acousto-optic** | SHG (λ→λ/2), SPDC (λ→2λ degenerate), acousto-optic Bragg deflection | SHG λ/2, SPDC energy, θ = λ·f_a/v_s |\n| **Quantum (analytic)** | Hong-Ou-Mandel dip, Bell **CHSH** | R(0)=0, \\|S\\|=2√2 |\n| **Physical-optics field engine** *(opt-in, off-trace — new in v0.12.0)* | real diffraction PSF (`wave_psf`), angular-spectrum free-space propagation + hologram reconstruction (`propagate_field`), Kolmogorov turbulence screens + seeing imaging (`turbulence_screen`/`propagate_turbulent`), split-step **NLSE** solitons (`propagate_pulse`), MCML tissue transport (`monte_carlo_tissue`), FDTD orchestration (`fdtd_derive_property`) | Airy 1.22 λF#, Strehl=exp(−(2πσ)²), w(z), D(r)=6.88(r/r₀)^{5/3}, soliton shape-invariance, R+T+A=1, TMM≡quarter-wave |\n\nTwo additions stay deliberately honest about their tier: **`detect_phenomena`** *flags* the conditions a\nphenomenon needs (it does not produce a diffractive image), and the **pyramid WFS** reports the wavefront\n*gradient* a pyramid integrates — a geometric slope, **not** a diffractive 4-pupil image (the chief-ray\ntracer does not propagate the pupil-plane field). Both are labelled Tier-1 in the code and docs.\n\nRun the self-test with a bare interpreter (no Blender needed):\n\n```bash\npython3 optical_alignment_sim/physics.py   # -> PHYSICS SELFTEST PASSED\n```\n\n**Catalog & meshes.** A built-in, **IP-clean component library** — **our own**, built to **real\nmarket norms**: **38 catalog entries** whose dimensions, specs, and mount conventions are modeled on\nindustry-standard parts (the kind you'd source from Thorlabs, Edmund, Newport, …), addressable by\nSKU, plus **118 documented real-world variants** in\n[docs/OPTICAL_ELEMENTS.md](docs/OPTICAL_ELEMENTS.md). **No vendor CAD is bundled** — vendor 3-D\nmodels are the vendors' IP. The library ships only **original metadata** (element type, port\ngeometry, mount parameters) and **correct generic mesh-free geometry** at true functional\ndimensions, so every entry is usable out of the box. Import your own CAD (STL/OBJ natively;\nSTEP/IGES via FreeCAD) to drop in an exact part.\n\n---\n\n## Install\n\nRequires **Blender 4.2 LTS or newer** (4.2+ / 5.x).\n\n### One-click — recommended, auto-updates\n\nOpen the **[install page](https://emircbngl.github.io/blender-optics-simulator/)** and drag the\n**“⤓ Drag this into Blender to install”** button onto an open Blender window. Blender installs the\nadd-on **and** subscribes you to update notifications in one gesture — no URL to type. (Make sure\n*Edit ▸ Preferences ▸ System ▸ Network ▸ Allow Online Access* is on.)\n\nEvery future release then appears **inside Blender** — a status-bar badge, then *Preferences ▸ Get\nExtensions ▸ Install Available Updates*. You never download a zip by hand again.\n\nThen open the **Optics** tab in the 3D viewport sidebar (press `N`).\n\n### From a GitHub zip — updates from inside the add-on\n\nDownload **`optical_alignment_sim-<version>.zip`** from the\n[Releases](https://github.com/emircbngl/blender-optics-simulator/releases) page and use *Edit ▸\nPreferences ▸ Add-ons ▸ Install from Disk…*. *(Or build from source: `blender --command extension build\n--source-dir optical_alignment_sim --output-dir .`)*\n\nBlender's own \"Install from Disk\" copies live in a **local** repository it never auto-syncs — but you\nstill get updates: the add-on's **Updates** panel (in the Optics sidebar) has an always-visible\n**\"Up to date · Check\"** control. Pressing **Check** registers the project's update channel for you and\npulls the newest version through Blender's native extension system — **no manual re-download, no\nre-install.** (Needs *Allow Online Access* on.)\n\n<a name=\"install--stay-updated\"></a>\n### Install & stay updated — every path is covered\n\n| How you installed | How you get updates |\n|---|---|\n| **One-click drag-link** (recommended) | Blender's native auto-update — new releases appear in *Get Extensions ▸ Install Available Updates* |\n| **GitHub zip / Install from Disk** | The in-add-on **Updates** panel → **Check** → **Install** → restart (self-subscribes to the channel on first check) |\n| **Built from source** | Same in-add-on **Updates** panel, or `git pull` + rebuild |\n\nEither way, the check runs at most **once a day** and only when *Allow Online Access* is enabled;\nnothing phones home otherwise.\n\n---\n\n## Quick start\n\n### From the UI\n\n- **Examples ▸** pick *Michelson* (or any of the 26) to spawn a full setup with the live beam overlay.\n- **Element** — select an object, *Tag as Optical Element*, *Auto-Detect Ports*; per-type parameters\n  appear (source polarization, waveplate angle, lens focal length, …).\n- **Mount & Adjustment** — *Apply Mount Preset* (e.g. KM100CP/M), *Set Coarse Pose*, drive the\n  tip/tilt knobs.\n- **Simulation** — toggle *Live simulation*; the beam updates as you move parts. *Start MCP Bridge*\n  to let an external agent drive the scene.\n- **Alignment Report** — *Update Report* / *Align* / *Align All*; detectors show measured power,\n  polarization, and fringe visibility.\n- **Adaptive Optics** — *Run AO Loop* to sense a wavefront and drive a deformable mirror flat.\n- **Render** — pick a camera + **Background** preset, then *EEVEE Preview* / *Cycles Final* (toggle\n  **Realistic optics** for glass + studio lighting); **Dress Bench** for the full table; **Export SVG\n  Schematic** for a publication-ready vector figure.\n\n### Headless / standalone scripts\n\n`examples/` contains builders that run with or without the add-on installed:\n\n```bash\nblender --background --python examples/agent_align.py    # the agent-align demo, headless\nblender --background --python examples/mach_zehnder.py\n```\n\n---\n\n## How this compares\n\nA **layout, alignment, visualization, and agent-control** tool — not a lens-design optimizer or a\nwave-optics solver. It sits in a niche the standard tools don't cover:\n\n| | What it does | Optics physics | Photoreal 3-D bench + opto-mech | **AI agent over MCP** |\n|---|---|:---:|:---:|:---:|\n| **Zemax / OpticStudio, CODE V** | sequential/non-sequential lens design + optimization (MTF, tolerancing) | full ray + diffraction | — | — |\n| **POPPY, diffractio, prysm** | physical / Fourier optics (PSFs, wavefront propagation) | full wave-optics | — | — |\n| **OptiCore** (Blender add-on) | generates precise optical-element *meshes* (lenses, mirrors) for rendering | element geometry only (no beam trace) | ✓ (meshes) | — |\n| **Blender** (alone) | gorgeous renders | none | ✓ | partial (geometry only) |\n| **This** | lay out → simulate → auto-align → render a real bench | single-ray + analytic overlays | ✓ | **✓ (full state as JSON)** |\n\nThe unfair advantage is the last column. Need a diffraction PSF or a tolerancing run? Reach for\nZemax/POPPY/prysm. Want physically precise lens/mirror *meshes* to render? [OptiCore][opticore] is\nexcellent at exactly that. This tool is the other half: it doesn't just place accurate geometry — it\nruns a **live beam** through it (ray + Gaussian-q + Jones/Stokes polarization), **auto-aligns** the\nbench with influence-matrix solvers, flags problems with `diagnose()`, and exposes the whole state so\nan **AI agent can drive it over MCP** — the **Blender × optics × MCP** intersection almost nobody\noccupies.\n\n[opticore]: https://github.com/CodeFHD/OptiCore\n\n---\n\n## How to cite\n\nIf you use **Blender Optics Simulator** in academic work, please cite it. A machine-readable\n[`CITATION.cff`](CITATION.cff) is included, so GitHub shows a **\"Cite this repository\"** button with\nready-to-paste APA / BibTeX.\n\n```bibtex\n@software{cobanoglu_blender_optics_simulator,\n  author  = {Çobanoğlu, Muhammet Emir},\n  title   = {Blender Optics Simulator},\n  year    = {2026},\n  version = {0.29.1},\n  doi     = {10.5281/zenodo.20778997},\n  license = {GPL-3.0-or-later},\n  url     = {https://github.com/emircbngl/blender-optics-simulator}\n}\n```\n\nThis software is archived on **Zenodo** with a citable DOI:\n[**10.5281/zenodo.20778997**](https://doi.org/10.5281/zenodo.20778997) — the *concept* DOI, which always\nresolves to the latest version. Each release also mints its own version DOI (see\n[`CITATION.cff`](CITATION.cff)).\n\n---\n\n## License & credits\n\n**GPL-3.0-or-later.** See [`LICENSE`](LICENSE). Vendor CAD/meshes are not included and remain the\nproperty of their respective owners; this project ships only original metadata, procedural geometry,\nand tooling.\n\nBuilt in the spirit of Bigweld's maxim from *Robots* (2005) — **\"See a need, fill a need.\"**\n",
  "bytes": 55575,
  "sha": "44737174856c7fb3f288aa2de4af24d8d3295c59e8d3a17d4d0d4b72d498c350",
  "repo_slug": "emircbngl/blender-optics-simulator",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_emircbngl_blender_optics_simul_7684ba5a/readme"
}