diff --git a/.claude/commands/regen.md b/.claude/commands/regen.md index b811617..22c533d 100644 --- a/.claude/commands/regen.md +++ b/.claude/commands/regen.md @@ -1,5 +1,5 @@ --- -description: Regenerate the five generated files (schemas, options.md, Python types and API docs) and check them +description: Regenerate the six generated files (schemas, options.md, materials-catalogue.md, Python types and API docs) and check them allowed-tools: Bash(dotnet build:*), Bash(dotnet src/UltrafastEngine.Cli/bin/*), Bash(python3 python/tools/*), Bash(git diff:*), Bash(mypy:*) --- Regenerate everything derived from the schema declarations, the Python docstrings and the test attributes: @@ -10,6 +10,7 @@ U="dotnet src/UltrafastEngine.Cli/bin/Release/net10.0/ultrafast.dll" $U schema system -o schemas/ufs.schema.json $U schema run -o schemas/run.schema.json $U reference -o docs/reference/options.md +$U reference materials -o docs/reference/materials-catalogue.md python3 python/tools/gen_types.py python3 python/tools/gen_api_docs.py dotnet build tests/UltrafastEngine.Tests -c Release -f net10.0 2>&1 | grep -E "rror\(s\)" @@ -17,5 +18,5 @@ dotnet tests/UltrafastEngine.Tests/bin/Release/net10.0/UltrafastEngine.Tests.dll (cd python && mypy --follow-imports=silent src/ultrafast/build.py src/ultrafast/types.py) ``` -Then `git diff --stat` on those five files and summarise what changed in one or two lines (new keys, renamed +Then `git diff --stat` on those generated files and summarise what changed in one or two lines (new keys, renamed types). Do not read the generated files in full; grep them for the keys you changed if you need to confirm. diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md index 4ee8df3..29ab304 100644 --- a/.claude/commands/verify.md +++ b/.claude/commands/verify.md @@ -5,7 +5,7 @@ allowed-tools: Bash, Read, Grep Run the complete verification. Delegate the long runs to the `test-runner` subagent so only the results come back. 1. `dotnet build UltrafastEngine.sln -c Release` (0 errors; the only expected warning is the ZOS-API stub warning). -2. Generated files current: run `/regen` steps and check `git diff --stat` shows no change in the five files. +2. Generated files current: run `/regen` steps and check `git diff --stat` shows no change in the six generated files. 3. C# tests: `dotnet tests/UltrafastEngine.Tests/bin/Release/net10.0/UltrafastEngine.Tests.dll --slow`; on machines with Mono also `mono tests/UltrafastEngine.Tests/bin/Release/net48/UltrafastEngine.Tests.exe --slow` (unset ULTRAFAST_STRICT_OPTIONS). 4. Python: `python3 python/tools/build_engine.py`, then in `python/`: diff --git a/.claude/hooks/after_edit.py b/.claude/hooks/after_edit.py index 05e7300..ca70949 100755 --- a/.claude/hooks/after_edit.py +++ b/.claude/hooks/after_edit.py @@ -9,7 +9,8 @@ import sys RULES = [ - ("src/UltrafastEngine.Core/Schema/BuiltIn*.cs", "Schema declaration changed: run /regen (5 generated files) before the tests; document the key in docs/reference/."), + ("src/UltrafastEngine.Core/Data/materials/*", "Catalogue file changed: run /regen (docs/reference/materials-catalogue.md) and, if a number moved, regenerate the pinned snapshot on purpose (ULTRAFAST_WRITE_SNAPSHOT=1, see CatalogueSnapshotTests)."), + ("src/UltrafastEngine.Core/Schema/BuiltIn*.cs", "Schema declaration changed: run /regen (6 generated files) before the tests; document the key in docs/reference/."), ("src/UltrafastEngine.Core/Schema/Migrations.cs", "Migration changed: keep the old key working and add a test in SchemaTests."), ("src/UltrafastEngine.Core/Physics/*", "Propagation physics changed: run the analytic tests (PropagationTests, NerTests) and, if scalar results may change, the pyNLO cross-checks (/physics-change)."), ("src/UltrafastEngine.Core/Transverse/*", "Spatial propagation changed: keep it consistent with Physics/Nonlinear.cs; run SpatialTests and NerTests."), diff --git a/.claude/hooks/protect_paths.py b/.claude/hooks/protect_paths.py index 78550b4..97011c2 100755 --- a/.claude/hooks/protect_paths.py +++ b/.claude/hooks/protect_paths.py @@ -18,6 +18,7 @@ ("schemas/*.schema.json", "Generated file: change src/UltrafastEngine.Core/Schema/BuiltIn*.cs and run /regen."), ("docs/reference/options.md", "Generated file: change src/UltrafastEngine.Core/Schema/BuiltIn*.cs and run /regen."), ("python/src/ultrafast/types.py", "Generated file: change the schema declarations and run /regen (python3 python/tools/gen_types.py)."), + ("docs/reference/materials-catalogue.md", "Generated file: change the catalogue files in src/UltrafastEngine.Core/Data/materials and run /regen."), ("docs/reference/python-api.md", "Generated file: change the Python docstrings and run python3 python/tools/gen_api_docs.py."), ("docs/reference/validation-matrix.md", "Generated file: change the [Test] / [TestArea] attributes and run /regen."), ("*/bin/*", "Build output: change the source instead."), diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d84992..cc08cbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,13 +21,14 @@ jobs: build-and-test: name: Build (net10.0, net48) and C# tests runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: global-json-file: global.json - name: Install Mono (net48 reference assemblies and runtime) - run: sudo apt-get update && sudo apt-get install -y mono-complete + run: sudo apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update && sudo apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 install -y mono-complete - name: Build everything run: dotnet build UltrafastEngine.sln -c Release - name: C# tests, .NET 10, including slow @@ -38,6 +39,7 @@ jobs: tests-macos-windows: name: C# and Python tests on ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -66,6 +68,7 @@ jobs: compile-platforms: name: Compile CLI for ${{ matrix.rid }} runs-on: ubuntu-24.04 + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -81,6 +84,7 @@ jobs: python: name: Python tests (hosted engine, pyNLO) runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 @@ -102,6 +106,7 @@ jobs: components: name: Component test scripts (numbers only) runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 @@ -125,6 +130,7 @@ jobs: papers: name: Paper replications (fast tier) runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 @@ -151,6 +157,7 @@ jobs: docs-and-examples: name: Docs and example check runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a08abb9..c52e70e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,6 +22,7 @@ jobs: build: name: Build the site (strict) runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 @@ -43,6 +44,7 @@ jobs: if: github.event_name != 'pull_request' needs: build runs-on: ubuntu-24.04 + timeout-minutes: 30 permissions: pages: write id-token: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..afe00b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,141 @@ +# Release assets on a version tag (v*): one .tar.gz per component and platform, manifest.json and install.py, attached to a GitHub release. +# docs/releasing.md. NO TAG IS CREATED BY ANYONE BUT THE OWNER. The workflow can also be started by hand (workflow_dispatch) to build and +# smoke-test the assets without publishing (the files stay as workflow artifacts). +# +# Jobs: `managed` builds everything that is pure .NET or plain files (the CLI for every platform is cross-published here, so macOS needs +# no macOS runner), `native` builds the NativeAOT components (C API, native wheel) on each OS they must be built on, `publish` merges the +# manifests, checks that no archive holds a ZOS-API file, installs a selection into a temporary folder, removes it again and uploads. +# macOS NativeAOT (cpp, python-native for osx-*) is built on the owner's Mac (./build.sh --components cpp python-native ...) and uploaded +# with `gh release upload`, because the macOS runner is not available to this repository (see ci.yml). +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + DOTNET_NOLOGO: "1" + +jobs: + managed: + name: Managed components and source folders + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - name: Build the assets + run: > + python build/package.py --out dist + --components cli python csharp plugins schemas docs examples media tests component-tests papers source + --platform win-x64 linux-x64 linux-arm64 osx-arm64 osx-x64 + - uses: actions/upload-artifact@v7 + with: + name: assets-managed + path: dist/release-*/* + if-no-files-found: error + + native: + name: NativeAOT components on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, rid: linux-x64 } + - { os: ubuntu-24.04-arm, rid: linux-arm64 } + - { os: windows-latest, rid: win-x64 } + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - name: Build the C API and the native wheel + run: python build/package.py --out dist --components cpp python-native --platform ${{ matrix.rid }} + - name: Smoke test, C example and ultrafast devices + shell: bash + run: | + set -e + mkdir -p smoke && cd smoke + python ../install.py --base-url ../dist/release-* --include cpp --dest cpp-install --no-pip + cmake -S cpp-install/cpp/example -B build -DULTRAFAST_DIR="$PWD/cpp-install/cpp" + cmake --build build --config Release + - uses: actions/upload-artifact@v7 + with: + name: assets-native-${{ matrix.rid }} + path: dist/release-*/* + if-no-files-found: error + + publish: + name: Check, smoke-test the installer, publish + needs: [managed, native] + runs-on: ubuntu-24.04 + timeout-minutes: 120 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - uses: actions/download-artifact@v8 + with: + path: parts + - name: Merge the job outputs into one release folder + run: | + mkdir -p release + # every job wrote manifest.json beside its assets; the assets have unique names, install.py is identical + for d in parts/*/; do cp -n "$d"*.tar.gz release/ 2>/dev/null || true; done + cp parts/assets-managed/install.py release/install.py + python build/release_components.py merge release/manifest.json parts/*/manifest.json + - name: No ZOS-API file in any archive or wheel (Ansys property) + run: python build/release_components.py check-zosapi release + - name: Planted file must be caught (the gate itself is tested) + run: python -m pytest -q python/tests/test_release.py -k "zosapi" + env: { PYTHONDONTWRITEBYTECODE: "1" } + - name: Smoke test, a selection into a temporary folder, then uninstall leaves it empty + run: | + set -e + python release/install.py --base-url release --platform linux-x64 --include cli schemas examples.Branched --dest "$RUNNER_TEMP/uf" --no-pip + "$RUNNER_TEMP/uf/bin/ultrafast" --version + "$RUNNER_TEMP/uf/bin/ultrafast" devices + ULTRAFAST_STRICT_OPTIONS=1 "$RUNNER_TEMP/uf/bin/ultrafast" run docs/examples/GettingStarted/spm-plate/plate.ufs docs/examples/GettingStarted/spm-plate/run.json -o "$RUNNER_TEMP/r" + python release/install.py --uninstall --dest "$RUNNER_TEMP/uf" --no-pip + test -z "$(ls -A "$RUNNER_TEMP/uf")" + - name: Smoke test, the wheel in a fresh environment lists devices and runs the quickstart + run: | + set -e + python release/install.py --base-url release --platform linux-x64 --include python --dest "$RUNNER_TEMP/py" --no-pip + python -m venv "$RUNNER_TEMP/venv" + "$RUNNER_TEMP/venv/bin/pip" install --find-links "$RUNNER_TEMP/py/python" "ultrafast-engine[hosted]" + "$RUNNER_TEMP/venv/bin/python" -c "import ultrafast; print(ultrafast.version())" + "$RUNNER_TEMP/venv/bin/python" -c "import ultrafast; print(ultrafast.devices())" + "$RUNNER_TEMP/venv/bin/python" docs/examples/GettingStarted/quickstart/quickstart.py + - name: Attach the assets (tag builds only) + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v3 + with: + files: | + release/*.tar.gz + release/manifest.json + release/install.py + draft: true + - uses: actions/upload-artifact@v7 + if: github.event_name == 'workflow_dispatch' + with: + name: release-folder + path: release/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b653dc..090aa64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,63 @@ ## Unreleased +**Fixed (physics)** +- **Avalanche and inverse bremsstrahlung were counted twice** (solids, where the avalanche is on by default): the avalanche loss `aval U / (2 k0 I)` equals `sigma rho / (2 k0)`, the Drude imaginary part, so the field lost the energy twice. The avalanche now only generates electrons; the field loses the energy once (`Plasma_Avalanche_DoesNotAbsorbTheFieldASecondTime`: output energy 5.15e-10 J, was 2.67e-10 J). Results of solid-state plasma runs with the avalanche on change. +- **Keldysh rate returned NaN in the far wing of a pulse** (field so weak that the Keldysh parameter exceeds 1e6): returns 0. +- **net48:** `ultrafast reference materials` printed `56.306399999999996` for `56.3064` (.NET Framework's `"R"`), so the committed page differed on Mono; the shortest round-trip form is used on both. + +**Added** +- `physics.strictMaterialsAllow`: catalogue paths (`"CS2.n2"`) or entry names that strict mode accepts although they are not verified. +- The generic defaults (thermal data of a user-defined gain medium: 10 W/(m K), 7.8e-6 1/K; ionisation inputs of a solid without data) are catalogue entries (`defaults:gainMedium`, `defaults:solidIonization`, status `unverified`); a run that uses one lists it in its notes and `unverifiedInputs`. +- Partial ionisation overrides: the run notes name the fields the run gave and say the rest come from the catalogue. + +**Changed (release polish)** +- **Component check "plasma blue shift"** (`tests/components/media.py`): the single-carrier Drude formula is compared with `frequencyResolved: false` at 5 %; the default frequency-resolved operator (WP4) is a separate row at 8 %, because the 1/omega dependence across a 30 fs bandwidth lowers the shift by up to 5 % (measured -5.3 % at 1030 nm). This was the one CI failure of the component scripts. +- **Tests:** the release-install test no longer asserts an executable bit on Windows. +- **CI:** job timeouts (a hung `apt-get` ran for two hours) and `apt-get` retries. +- **Docs:** logo-palette theme and the long logo at the top of every page; README picture grid and contact badge; `docs/design/` condensed to `history.md`, `next-steps.md` and `backlog.md` (the branching design, the AO / twin record, the plan and the session notes are removed). + +**Added (session F, WP8: release system)** +- **Release assets by component:** `build/package.py --components NAME...` (the old `--cli --python --cpp --csharp --zemax --examples --docs` are aliases) writes `dist/release-/`: one reproducible `.tar.gz` per component and platform, `manifest.json` (size, SHA-256, platform, dependencies for each asset) and `install.py`. Components: `cli`, `python`, `python-native`, `cpp`, `csharp`, `zemax`, `plugins.`, `schemas`, `docs`, `examples.` + `examples.shared`, `media` (not in `all`), `tests`, `component-tests`, `papers`, `source`. Source components are built from `git ls-files` (`build/release_components.py`). +- **`install.py`** (standard library only; in the repository and attached to each release): `--list`, `--include` / `--exclude` (dotted names, down to `examples..`; dependencies come along), `--version`, `--platform`, `--dest`, `--dry-run`, `--uninstall`, `--source git` (partial clone with a cone-mode sparse checkout). Hash-checked before anything is written; the install record `.ultrafast-install.json` makes uninstall remove exactly the files it wrote (a changed file is left with a warning); a second `--include` adds, a newer `--version` upgrades; wheels go in with `pip --find-links`, never from PyPI. +- **ZOS-API gate:** the asset writer refuses `ZOSAPI*` files, the packager and `release_components.py check-zosapi` look inside every archive and wheel, the installer refuses an archive that holds one. +- **`.github/workflows/release.yml`** on a `v*` tag (draft release): managed assets and the CLI for five platforms on Linux, NativeAOT components on Linux x64 / arm64 and Windows, manifest merge, the gate, installer smoke test. Not yet run on GitHub; macOS NativeAOT is built on the owner's Mac. No tag was created. +- 23 Python tests (`test_release.py`, no network or .NET): resolution, install / uninstall against a local release, changed files, hash mismatch, upgrade, the gate with a planted file, the git sparse path in a temporary repository, the packager options. Notes: [session-f.md](docs/design/notes/session-f.md). + +**Added (session E, WP7: the open findings)** +- **Raman shift rate explained (`Fibres/raman-soliton-shift`):** the engine's rate is within 2 % of the exact-gain rate of a fixed-width soliton (`-gamma f_R integral (h_R * P) P' dt / E`, which reduces to Gordon's formula for a derivative-of-delta response) and within 0.3 % of pyNLO; Gordon's first-moment formula is 15-39 % low for 40-65 fs solitons because the soliton spectrum is not narrow against the Raman gain. No engine change. Test `test_raman_shift.py`. +- **SLM against Fourier explained (`AdaptiveOptics/slm-beam-shaping`):** a panel centred on the grid has a pixel edge on the sample at the axis, so each grid cell averaged two pixels and a blaze of P pixels per period kept `cos^2(pi/P)` of its light (0.854 at 8, 0.962 at 16). With `offsetXM = offsetYM = -pitchM / 2` the panel keeps all of it (1.0000) and the focal field equals an exact angular-spectrum propagation of the sampled field to 0.9-1.0 % (12-14 % with the absorbing edge, which removes the replica orders the periodic reference wraps round the window). The example now uses the offset and the exact reference. **New warning** in the modulator ("averages two device cells") when the pitch equals the grid step and the cell edges fall on grid points. Tests in `test_ao_plugin.py`. +- **Tiled-array SPGD reference explained (`AdaptiveOptics/tiled-array`):** at f = 0.5 m on 128 cells the lens phase at a tile 3 mm out changes by 3.7 rad per cell, so the lens was under-sampled and identical tiles reached the axis with fluences differing by a factor of 56 (the lock ended at 1.02 of the in-phase value). The lock now runs at f = 1 m (1.9 rad per cell) and ends at 1.00 with both bounds asserted. Test `test_tiled_array_identical_tiles_reach_the_axis_alike_only_when_the_lens_is_resolved`. Notes: [session-e.md](docs/design/notes/session-e.md). + +**Added** +- **ASE over the inversion lifetime (session D, WP2):** `aseModel` on gain and amplifier elements (`true` or `{solidAngleSr, polarizations, points, bandLowerM, bandUpperM}`): steady-state forward and backward ASE per transverse cell and frequency of the emission band, quasi-static in z at frozen N2, depleting the inversion in the pumping and decay between pulses (so also in the pulse-train steady state). New file `Gain/AseModel.cs`, shared with fibre gain later. Reports `aseForwardPowerW`, `aseBackwardPowerW`, the spectra, `aseStimulatedPowerW`, `aseDepletionFraction`, `asePedestalEnergyJ` / `asePedestalEnergyFraction` (the strength for a `pulse.pedestal` of kind `ase`). Opt-in; `"ase": true` (noise in the window) is unchanged, no existing result moved. Transverse-resolved inversion yes, rays crossing cells no ([gain.md](docs/reference/gain.md#ase)). +- **Stress-birefringence pattern:** `thermalLens.stress` on a cartesian transverse grid (`ny > 1`) with an x-polarised input applies the four-lobed Jones map R(-phi) diag(1, e^{i delta}) R(phi) per point; the carried profile gets its own y shape; new value `depolarizationFractionPattern`. Its beam average equals the lumped fraction (1e-6). **Changed behaviour:** a spatial gain element on a cartesian grid with `stress` used the lumped rotation before; it now uses the pattern (the radial grid and a y input keep the lumped form, with a note). +- 9 tests (`AseStressTests`): n_sp h nu dnu (G-1) at 1e-12, RK4, Fuchtbauer-Ladenburg, saturated-amplifier law, cw photon balance, stress map. Notes: [session-d.md](docs/design/notes/session-d.md). + +**Added (earlier in this iteration)** +- **Material catalogue (session C, WP3):** every material number of the engine now lives in a JSON catalogue file with its unit in the key name, `source`, `status` (`verified` | `approximate` | `unverified`), uncertainty and conditions: `src/UltrafastEngine.Core/Data/materials/{solids,gases,raman}` (41 entries, 115 quantities), embedded in the library. `MaterialLibrary`, `GasData`, `GasIndex`, `GasFormulaIndex`, `SolidIonizationData` and the Raman models (`LinAgrawalRamanResponse`, `HollenbeckCantrellRamanResponse`, the registry defaults) read from it through `Catalogue` / `CatalogueModels`; no table is left in C#. The move was a pure refactor: every index, n2, Raman kernel, rotational line and ionisation datum is pinned (`CatalogueSnapshotTests`, 2963 values) and reproduced to 1e-15. +- **Runs say what they used:** a nonlinear element that uses an `approximate` or `unverified` catalogue value (n2, Raman response and fraction, Kerr anisotropy, rotational Raman data, ionisation data) adds a note (`APPROXIMATE catalogue value used: FusedSilica.n2 ...`), and `summary.json` has the new key `unverifiedInputs`; `provenance.json` entries carry `catalogueStatus` / `catalogueSource`. A value the user overrode is not listed; a linear run lists an unverified index. +- **User catalogues:** `~/.ultrafast/materials/*.json` (or `$ULTRAFAST_MATERIALS`) add a material or replace single quantities of a built-in one, with a notice and a note in the run. +- `ultrafast reference materials -o docs/reference/materials-catalogue.md` generates the catalogue page (a sixth generated file; `/regen`, the protect hook and the up-to-date test know it). It ends with the "not verified yet" list. +- Dispersion sets checked against the refractiveindex.info pages (fused silica, N-BK7, N-SF11, CaF2, sapphire o/e, YAG, MgF2 o/e, quartz o/e, calcite o/e, BaF2, CS2, the Borzsonyi gases, O2, CO2, H2): 28 quantities are now `verified`, each with the comparison recorded. +- **Gain media, chi(2) crystals and Ciddor's constants moved into the catalogue (session C, part 2):** `Data/materials/gain` (Yb:YAG, Ti:sapphire, Nd:YAG: 3 entries) and `Data/materials/crystals` (BBO, LBO, KTP, KDP, LiNbO3, MgO:LiNbO3, BiBO, AgGaSe2: 8 entries) read by `GainMedium.Get` and `CrystalLibrary.Get`; the constants of `CiddorAirIndex` are quantities of the `Air` entry. Pure refactor again: 449 more pinned values (cross sections and saturation fluences of the three media at two temperatures, the principal indices, d tensors and scalars of every crystal at two temperatures, the air index and water-vapour fraction in three states) reproduced to 1e-15. Gain and crystal elements list their approximate / unverified catalogue values in the run notes. 15 more quantities are `verified`: the BBO, KTP, KDP, LiNbO3 (o), BiBO and AgGaSe2 index sets equal the refractiveindex.info pages; Ciddor's dry-air dispersion equals the standard-air page (test). +- **Strict mode:** `physics.strictMaterials` (or `ULTRAFAST_STRICT_MATERIALS=1`) refuses a run that uses an approximate or unverified catalogue value and lists them in the error. +- Tests: `CatalogueSnapshotTests`, `CatalogueTests` (see `docs/testing.md`). +- **Thermal lens as a distributed lens (session B, WP1):** the tracked beam crosses each slice of a pumped gain medium as a graded-index duct n(r) = n0 (1 - g² r²/2) with the exact reduced ABCD matrix (`BeamState.Duct`, `DuctMatrix`) instead of thin lenses. A regenerative amplifier can put its thermal lens on the tracked beam (`thermalLens.track`, `passGapM`, an integer `roundTrips`). Elements report `trackedThermalLensEffectiveFocalLengthM` (-1/C of the duct) beside the thin-lens equivalent `trackedThermalLensFocalLengthM`. +- **Frequency-resolved Drude plasma (session B, WP4):** `PlasmaResponse` implements the new `IDispersiveResponse`; the envelope, carrier-resolved and spatial propagators apply the free-electron index as the spectral operator `i (omega/c) R(omega) FFT[dn A]` with `R = omega0 (omega0 + i nu) / (omega (omega + i nu))`. `ionization.frequencyResolved` (default true; false = the carrier value of earlier versions) and `ionization.electronDensityM3` (a prepared plasma). New `Physics/DispersiveResponses.cs`. +- Tests: the duct matrix, its limits and convergence, a thick rod and regenerative round trips (`BeamTrackingTests`); the Drude filter of a prepared plasma in three field models, its narrow-band limit, the drift energy of electrons born in the field, envelope against carrier-resolved (`IonizationTests`). +- **`UF_WITH_AO`:** the `ao` plugin (`deformableMirror`, `slm`, `spectralShaper`, `aopdf`) is compiled into the NativeAOT library like `gpu` and `hitran`; `build/package.py` passes `UF_WITH_AO=false` when it is not selected. +- **Grid planner and shapers:** a `spectralShaper` with a stepped or gapped mask gets room for its side pulses (orders 1 to 8 at `2 pi / pixel bandwidth`, weighted by the spot size); an `aopdf` gets room for its delay, replica delay and `|GDD| x bandwidth`. With `grid.auto: false` the planner warns instead. New `CompiledElement.ProgrammedGddS2` and a `programmedGddS2` argument of `GridPlanner.Plan`. The Weiner example no longer needs `grid.windowS`. +- The unknown-element error names the `ao` plugin for its four element types. +- A Branched tree of four arms, recombined at one point, against the tile source on axis (`|sum exp(i phi)|^2 / 16`); documented the closed speed items (compile cache, streaming executor) with their revisit triggers in `docs/reference/speed.md`. +- Tests for the open edges of the `ao` plugin: the shaper's grating dispersion against the Littrow reference; Bolduc and double-phase hologram maps with a varying complex field through the `slm` element; tiled-array lobe fractions against the array factor. + **Changed** +- **TGG refractive index:** replaced the old two-point fit (up to 0.0106 away from the measured table) by a one-pole fit to the nine points of Schlarb and Sugg 1994 (largest deviation 6.6e-4, valid 0.40-1.18 um, `approximate`). The YVO4 vendor Sellmeier sets differ from Shi et al. 2001 by about 5e-4 (o) and 9e-4 (e); left as they are and recorded in the catalogue for the owner to decide. +- **A measured `thermalLens.focalLengthM` is the effective focal length** (-1/C of the whole medium, what is measured from outside) and is spread over the medium as one uniform duct; before it was one thin lens at the centre (the two differ by f (gL)²/6, 2e-4 for a 1 mm rod). The tracked radius behind a pumped rod moves by about (gL)²: 0.4 % for the 4 mm rods of the examples. +- **The Drude plasma term is frequency-resolved in all three models:** the 1/(omega (omega + i nu)) dependence across the pulse bandwidth is included (it was the carrier value in envelope mode and omega/omega0² in carrier-resolved mode). For a broad pulse the blue shift is smaller (a 10 fs, 2 µm pulse in argon: 7 % instead of 12 % at 2 cm), and the electrons born in the field now take their drift energy U_p (the `absorption` loss grows by about U_p/U_i: 17 % in the filament example, `Nonlinear/filament`). `frequencyResolved: false` restores the old behaviour. Note: `trackedThermalLensFocalLengthM` is the thin-lens equivalent n0 g² L; with a measured focal length it no longer equals the given value (use `trackedThermalLensEffectiveFocalLengthM`). +- **cw sources sit on the requested carrier:** the lattice is shifted so that a bin is the carrier (it used to be snapped to the nearest multiple of the frequency step, 1.2e-6 away at 1030 nm). A cw run and a one-stored-frequency pulse run at the same carrier now give the same device response (patterns agree to 1e-16 instead of 3e-6; with four stored frequencies to 1.3e-8, the pulse bandwidth). A single stored frequency of a pulse is its energy-centroid frequency rather than the lattice bin next to it. No reference test needed re-baselining; the two cw-against-pulse tests were tightened (2e-5 to 1e-7, 3e-6 to 1e-9). Carrier-resolved models with a cw source need `grid.snapCarrier: true` written out. +- **`sensorless.spgd` is scale-free by default** (`gain=None`): the update is normalised by the recent range of the metric difference, the step of a channel is at most `step` (0.4) x `amplitude`, and multiplying the metric by a constant changes nothing. The default reaches at least 0.94 of the optimum for `step` 0.3 to 1.0 on phase locking and a Strehl peak at metric scales 1e-3 to 1e3, and 0.9 on the engine's deformable-mirror bench. `gain=` keeps the classical update (gain in units of 1 / metric), so existing calls behave as before; the fixed gains that lock at scale 1 do nothing at 1e-3 or 1e3. - **Python package structure:** `ultrafast.plotting` is a package (`style`, `data`, `time_domain`, `spectral`, `run_figures`, `spatial`, `instrument_panels`, `diagrams`, `zernike`, `animate`); `ultrafast.diagram` moved to `ultrafast.plotting.diagrams` (the old name still imports and warns). New `ultrafast.system`, `components`, `sources`, `instruments`, `algorithms` and `results` group the builders by what they are (CapWords names are aliases of the `build` functions, so typed returns are unchanged). Every 2.2.0 public name still imports (a test checks a committed list). - **Examples regrouped** from `docs/examples/NN-name` into `docs/examples//` (GettingStarted, LaserSystems, Nonlinear, Diffraction, Wavefronts, Branched, Diagnostics, Components, Computing, DigitalTwin, Interop, AdaptiveOptics); the index keeps the old numbers; `build/example_check.py` accepts a name, a group or an old number. - CI: `checkout` v7, `setup-python` v7, `setup-dotnet` v6, `upload-artifact` v7 (Node 24); `pyproject.toml` uses an SPDX license string (setuptools >= 77). @@ -10,6 +66,7 @@ - **CI:** the unavailable `macos-14` job is replaced by `ubuntu-24.04-arm` (arm64 Linux, the stand-in for Apple silicon). macOS is verified by hand. **Fixed** +- **`ny = 1` grids and one-row maps:** a `measured` profile or `mask` file whose map has a single row (for example a "1-D" scene saved at `y = (j - ny/2) dx` with `ny = 1`, whose row is at `y = -dx/2`) gave zero on a grid with `ny = 1`, because the grid samples `y = 0`, outside the one-row map. An axis with one sample is now constant along that axis. - `transverse.spectralSamples: 1` stored the profile at the low edge of the band (the first bin above 1e-6 of the peak, 3e-6 off the carrier for a 1 ns pulse); it is now the bin nearest the energy centroid. Single-frequency spatial results move by up to a few 1e-6. The Fox-Li test that relied on the offset now starts from a curved-mode plane (unequal gaps), and checks that an eigenmode start converges on the analytic mode radius. **Added** diff --git a/CLAUDE.md b/CLAUDE.md index 390f652..8872a47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ after a code change. ## Token economy * **Do not read generated files** -- they are large and derived: `schemas/*.json` (440 kB), `python/src/ultrafast/types.py` - (180 kB), `docs/reference/options.md` (95 kB), `docs/reference/python-api.md`, `docs/reference/validation-matrix.md`. Read the source instead + (180 kB), `docs/reference/options.md` (95 kB), `docs/reference/materials-catalogue.md`, `docs/reference/python-api.md`, `docs/reference/validation-matrix.md`. Read the source instead (`src/UltrafastEngine.Core/Schema/BuiltIn*.cs`, `python/src/ultrafast/build.py`, the test attributes) and regenerate with `/regen`. Edits to them are blocked by a hook. * Never read `bin/`, `obj/`, `artifacts/`, `dist/`, `site/`, `results*/`, `*.npz`, `CHANGELOG.md` history. @@ -65,7 +65,7 @@ after a code change. | speed: CPU FFT providers, compute back-ends, GPU | `docs/reference/speed.md`, `docs/plugins/gpu.md`; tests `ComputeTests`, `GpuTests` | | tests | `tests/UltrafastEngine.Tests/CLAUDE.md` | | status / what is verified | `PROJECT_STATUS.md`; history in `CHANGELOG.md` (top section only) | -| current plan, owner decisions | `docs/design/plan-next.md` (work on `dev/*` branches; merge to `main` after `/verify` + green CI) | +| current plan, owner decisions | `docs/design/next-steps.md` (open items), `docs/design/backlog.md` (parked), `docs/design/history.md` (what was built) (work on `dev/*` branches; merge to `main` after `/verify` + green CI) | | paper replications | `docs/papers/` (`docs/papers/CLAUDE.md`): published headline results reproduced, plots + commentary | | component checks with plots | `tests/components/` (`tests/components/CLAUDE.md`): one script per component family, HTML gallery | diff --git a/HANDOFF.md b/HANDOFF.md index c075a1c..ac08c4e 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -164,4 +164,4 @@ DOIs for the first four were given from memory: check them before downloading. in `python/tests/test_callbacks.py` is explained in its comment: the value was 1.99 % off before the changes too). * **Generated files** (`schemas/*.json`, `python/src/ultrafast/types.py`, `docs/reference/options.md`, `python-api.md`, `validation-matrix.md`) are regenerated with `/regen` and checked by tests; do not edit them by hand. -* `docs/design/plan-fixes-2026-11.md` records the fix plan with each stream's notes and changelog lines; it is complete. +* The November 2026 fix plan was completed and removed (2026-11-10); its outcome is in the changelog and in 2.2.0. diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index fcef3a1..1b0d013 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -8,9 +8,9 @@ What comes next: [ROADMAP.md](ROADMAP.md). Changes: [CHANGELOG.md](CHANGELOG.md) | check | result | |---|---| | `dotnet build UltrafastEngine.sln -c Release` (net10.0 and net48) | 0 errors; the only warning is the expected missing ZOS-API (the Zemax binding then builds against stubs) | -| C# tests, `--slow` | **544 pass, 0 fail, 9 skipped** (8 `GpuTests` without a GPU device; the opt-in HAPI test) | -| Python tests (hosted engine, `ULTRAFAST_SLOW=1`, with pyNLO and LightPipes) | **216 pass, 1 skipped** | -| Component tests (`tests/components/run_all.py`: 10 families covering linear optics, splitters, polarisation, crystals, nonlinear media, spatial components, instruments) | **1688 / 1691 checks** pass against analytic answers; **3 are marked KNOWN** (a 1.5-cycle pulse broadened to zero frequency at 2 µm / 10 fs, reported by a warning), 0 fail | +| C# tests, `--slow` | **585 pass, 0 fail, 9 skipped** (8 `GpuTests` without a GPU device; the opt-in HAPI test) | +| Python tests (hosted engine, `ULTRAFAST_SLOW=1`, with pyNLO and LightPipes) | **251 pass, 2 skipped** (one needs `ULTRAFAST_NATIVE_LIB`; run 2026-11-11 with `ULTRAFAST_LIBRARY` set to the hosted engine, otherwise a stale `bin/native` shadows it and the plugin tests fail; includes the 23 release tests) | +| Component tests (`tests/components/run_all.py`: 10 families covering linear optics, splitters, polarisation, crystals, nonlinear media, spatial components, instruments) | **1691 / 1694 checks** pass against analytic answers; **3 are marked KNOWN** (a 1.5-cycle pulse broadened to zero frequency at 2 µm / 10 fs, reported by a warning), 0 fail | | Paper replications (`build/paper_check.py`) | 12 folders, 0 hard failures (fast tier); each README ends with *How well the science matched*; the soft checks mark what does not reproduce ([index](docs/papers/README.md)) | | Examples and tutorials (`build/example_check.py`, strict options) | 0 problems (60 example folders in 13 groups, 35 with a committed hero figure; the new AdaptiveOptics, DigitalTwin and Fibres examples checked on the last run); the C++ example and the OpticStudio export (`Interop/`) are manual | | `mkdocs build --strict` | 0 warnings | @@ -29,7 +29,7 @@ example check. |---|---| | GPU back-ends (CUDA, OpenCL) | need a device with FP64; the tests skip without one (this includes the S2 parity test of the modulator and tile source, and the `ao` elements) | | MCP server for agents (twin plugin, T6) | designed, **not built**; `ultrafast twin serve` exits with 2 | -| NativeAOT library with the `ao` plugin | `UF_WITH_AO` is not set in the native project: the hosted engine (Python default, CLI) loads the plugin, the native library does not | +| NativeAOT library with the `ao` plugin | compiled in (`UF_WITH_AO`); the check that it lists the four elements (`test_native_library_lists_the_four_ao_elements`) runs only when `ULTRAFAST_NATIVE_LIB` points at a built library | | OpticStudio / Zemax | needs a Windows machine with OpticStudio; the binding is built against stubs in CI | | Release packages | built and smoke-tested per platform with `build.sh` on request; no release workflow yet | | macOS in CI | not run on GitHub (the `macos-14` runner configuration is not available to this repository); the macOS arm64 build and tests are run by hand on the development machine, and the Linux arm64 job covers the arm64 code paths | diff --git a/README.md b/README.md index fb82735..0066d18 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+Email +

+

@@ -17,9 +21,9 @@ License: Apache-2.0

-C# tests -Python tests -Component checks +C# tests +Python tests +Component checks Paper replications

@@ -40,11 +44,17 @@ It is built for people who design and operate table-top and open-air laser syste or changing a beamline, what the pulse will look like at the end, and why a measurement differs from the design. The focus is free-space optics, with single-mode fibres and gas cells where a system needs them. + + + + +
A complete Yb laser system
A complete Yb laser system
Filament in air
Filament in air
Non-collinear OPA
Non-collinear OPA
Four-grating compressor
Four-grating compressor
Carrier-resolved propagation
Carrier-resolved propagation
Tiled-aperture combining
Tiled-aperture combining
Focusing through a scatterer
Focusing through a scatterer
A digital twin
A digital twin
Fringes from a tilted mirror
Fringes from a tilted mirror
+ **Why use it** * **One model from design to measurement.** The same system file gives the pulse, the loss budget and the traces your diagnostics would record. A fitting plugin compares them with your data and finds the cause of the difference. -* **Validated, not just plausible.** Every component is run alone against its analytic answer (1691 checks), the +* **Validated, not just plausible.** Every component is run alone against its analytic answer (1694 checks), the suite pins the physics to published formulas and to an independent code ([pyNLO](https://github.com/pyNLO/PyNLO)), and twelve published experiments are replicated, with an honest account of what agrees and what does not. * **Runs anywhere, no dependencies.** A C# core (.NET 10 and .NET Framework 4.8 for OpticStudio) with no NuGet @@ -160,7 +170,7 @@ The full documentation is at ** list[tuple[str, Path]]: + """Files of a built folder as (archive path, file); ``sub`` is the folder inside ``root``, ZOS-API / symbol files never.""" + base = root / sub + out = [] + if base.exists(): + for f in sorted(base.rglob("*")): + rel = f.relative_to(root).as_posix() + if f.is_file() and keep(rel) and not rc.forbidden(rel) and not any(fnmatch.fnmatch(f.name, pat) for pat in NEVER): + out.append((rel, f)) + return out + + +def build_release(a, builder: "Builder") -> Path: + """Write ``/release-/``: one ``-[-].tar.gz`` per selected component, ``manifest.json`` and + ``install.py``. Built components come from the same Builder steps as the single-folder packages; source components from ``git ls-files``.""" + ver = builder.ver + defs = rc.definitions(REPO, list(PLUGINS)) + chosen, _, _, notes = _installer.expand(defs, a.components, a.exclude) + for n in notes: + Log.warn(n) + names = sorted(chosen) + out = builder.out / ("release-" + ver) + print("release assets: %s" % ", ".join(names)) + print("platforms (built components): %s" % ", ".join(a.rids)) + if Log.dry: + for n in names: + print(" %-28s %s" % (n, "built" if defs[n]["kind"] == "built" else "from git")) + return out + if out.exists(): + shutil.rmtree(out) + out.mkdir(parents=True) + assets: dict[str, list[dict]] = {} + files = rc.tracked_files(REPO) + + def add(name, plat, fl): + if not fl: + Log.warn("component %s (%s) came out empty: not written" % (name, plat)) + return + info = rc.write_asset(fl, out / rc.asset_name(name, ver, plat)) + assets.setdefault(name, []).append(dict(platform=plat, **info)) + print(" %-28s %-12s %9.2f MB" % (name, plat, info["size"] / 1e6)) + + for n in names: + if defs[n]["kind"] == "source": + add(n, "any", rc.collect(REPO, n, defs, files)) + built = [n for n in names if defs[n]["kind"] == "built"] + done_any: set[str] = set() + for rid in a.rids: + stage = builder.cache / ("stage-" + rid) + stage.mkdir(parents=True, exist_ok=True) + per_rid = lambda n: defs[n].get("perPlatform") or n not in done_any # platform-independent ones once + if "cli" in built: + builder.cli(rid, stage) + add("cli", rid, _slice(stage, "bin", lambda r: not r.startswith("bin/plugins/"))) + for n in [n for n in built if n.startswith("plugins.")]: + if per_rid(n): + builder.build_plugins() + pid = n.split(".", 1)[1] + src = REPO / "artifacts" / "plugins" / pid + fl = [("bin/plugins/%s/%s" % (pid, f.relative_to(src).as_posix()), f) for f in sorted(src.rglob("*")) + if f.is_file() and not rc.forbidden(f.name) and "tools" not in f.relative_to(src).parts and not f.name.endswith(".pdb")] + add(n, "any", fl); done_any.add(n) + if "csharp" in built and per_rid("csharp"): + builder.csharp(rid, stage) + add("csharp", "any", _slice(stage, "csharp")); done_any.add("csharp") + if "python" in built and per_rid("python"): + a.python_engine = "hosted" + builder.python(rid, stage) + add("python", "any", _slice(stage, "python", lambda r: r.endswith(".whl"))); done_any.add("python") + if "python-native" in built: + a.python_engine = "native" + nstage = builder.cache / ("stage-native-" + rid) + builder.python(rid, nstage) + add("python-native", rid, _slice(nstage, "python", lambda r: r.endswith(".whl"))) + if "cpp" in built: + builder.cpp(rid, stage) + add("cpp", rid, _slice(stage, "cpp")) + if "zemax" in built: + builder.zemax(rid, stage) + add("zemax", rid, _slice(stage, "zemax")) + m = rc.manifest(ver, assets, defs) + # only components that were asked for are kept in the manifest (the rest of the registry is not part of this release) + keep = set(names) | {c for c in m["components"] if any(c.startswith(x + ".") for x in names)} + m["components"] = {c: v for c, v in m["components"].items() if c in keep} + (out / "manifest.json").write_text(json.dumps(m, indent=1), encoding="utf-8") + shutil.copy2(REPO / "install.py", out / "install.py") + bad = rc.check_archives([out]) + if bad: + raise SystemExit("ZOS-API files found in the release (Ansys property, never packaged): " + ", ".join(bad)) + print("manifest: %s (%d components, ZOS-API check clean)" % (out / "manifest.json", len(m["components"]))) + return out + + def parse(argv): ap = argparse.ArgumentParser(prog="build", description="Compile and package UltrafastEngine release folders (docs/releasing.md).", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -682,6 +785,11 @@ def parse(argv): ap.add_argument("--csharp", action="store_true", help="UltrafastEngine.Core.dll (net10.0, net48) + plugin assemblies") ap.add_argument("--zemax", action="store_true", help="OpticStudio extension (Windows; needs your ZOS-API; on by default with the zemax plugin and --all)") ap.add_argument("--all", action="store_true", help="every component") + ap.add_argument("--components", nargs="*", default=None, metavar="NAME", + help="release assets: one .tar.gz per component and platform + manifest.json + install.py in /release-/ " + "(names: docs/releasing.md, e.g. cli python plugins examples.Branched media tests; 'all' = all but media). " + "--cli --python --cpp --csharp --zemax --examples --docs add their component names") + ap.add_argument("--exclude", nargs="*", default=None, metavar="NAME", help="with --components: names to leave out") ap.add_argument("--plugins", nargs="*", default=None, metavar="ID", help="plugins to include: fitting (alias transform), zemax, all, none (default: all)") ap.add_argument("--platform", "--platforms", nargs="*", default=None, dest="platforms", metavar="RID", help="target platforms (default: this machine)") ap.add_argument("--framework-dependent", action="store_true", help="CLI needs an installed .NET 10 runtime (smaller; default: self-contained)") @@ -701,9 +809,16 @@ def parse(argv): ap.add_argument("-v", "--verbose", action="store_true") a = ap.parse_args(argv) a.self_contained = not a.framework_dependent + a.release_mode = a.components is not None + if a.release_mode: # the old flags are aliases for component names + extra = [n for n, on in (("cli", a.cli), ("python", a.python), ("cpp", a.cpp), ("csharp", a.csharp), ("zemax", a.zemax), + ("examples", a.examples), ("docs", a.docs), ("all", a.all)) if on] + a.components = list(a.components) + extra + if not a.components: + ap.error("--components needs at least one name (see docs/releasing.md)") if a.all: a.cli = a.python = a.cpp = a.csharp = a.zemax = True - if not (a.cli or a.python or a.cpp or a.csharp or a.zemax): + if not (a.cli or a.python or a.cpp or a.csharp or a.zemax or a.release_mode): a.cli = True # plugins names = ["all"] if a.plugins is None else (a.plugins or ["none"]) @@ -756,6 +871,16 @@ def main(argv=None): print(" C API and native Python only for %s targets (NativeAOT does not cross-compile between OSes)." % host_os()) return 0 b = Builder(a) + if a.release_mode: + try: + build_release(a, b) + finally: + shutil.rmtree(b.cache, ignore_errors=True) + if Log.warnings: + print("\n%d warning(s):" % len(Log.warnings)) + for w in Log.warnings: + print(" - " + w) + return 0 print("UltrafastEngine %s -> %s" % (b.ver, b.out)) print("components: %s | plugins: %s | platforms: %s" % ( ", ".join(k for k in ("cli", "python", "cpp", "csharp", "zemax") if getattr(a, k)), ", ".join(a.plugins) or "none", ", ".join(a.rids))) diff --git a/build/release_components.py b/build/release_components.py new file mode 100644 index 0000000..5bbb9b2 --- /dev/null +++ b/build/release_components.py @@ -0,0 +1,255 @@ +"""Release components: the names ``--components`` (build/package.py) and ``install.py --include`` accept, what each contains, and the +asset writer. Standard library only. + + definitions(repo, plugin_ids) -> {name: definition} what exists: description, requires, kind, source paths ... + collect(repo, name, defs) -> [(archive path, file)] the files of a source component (tracked files only) + write_asset(files, out_file) -> {file, size, sha256} a .tar.gz with fixed timestamps, ZOS-API guard + manifest(version, assets, defs) -> dict what install.py reads (manifest.json) + merge(manifests) -> dict the manifests of several build jobs -> one + forbidden(name) -> bool True for Ansys ZOS-API assemblies (never packaged) + +Component names: ``cli`` ``python`` ``python-native`` ``cpp`` ``csharp`` ``zemax`` (built, per platform or platform-independent), +``plugins.``, ``schemas``, ``docs``, ``examples.`` + ``examples.shared``, ``media``, ``tests``, ``component-tests``, +``papers``, ``source``. A name selects its children (``examples`` = every group). Files of source components are repository-relative +(``docs/examples/Branched/...``), the same tree as a git sparse checkout. Command line: ``python build/release_components.py merge OUT IN...`` +and ``... check-zosapi PATH...``. Guide: docs/releasing.md. +""" +from __future__ import annotations + +import fnmatch +import gzip +import hashlib +import io +import json +import subprocess +import sys +import tarfile +from pathlib import Path + +MEDIA_EXT = (".png", ".gif", ".svg", ".jpg", ".jpeg", ".mp4", ".webm") +MEDIA_ROOTS = ("docs/examples/", "docs/papers/") +# never packaged: not part of the product (agent memory), caches, licensed paper PDFs, build leftovers +SKIP_PARTS = ("__pycache__", "bin", "obj", "results", ".git") # only for the no-git fallback walk +SKIP_NAMES = ("CLAUDE.md", "CLAUDE.local.md", ".DS_Store") + + +def forbidden(name: str) -> bool: + """Ansys ZOS-API assemblies (``ZOSAPI*.dll`` / ``.xml`` / ``.pdb``, any case). The tracked ``ZosApiStubs`` sources are ours and allowed.""" + n = name.replace("\\", "/").rsplit("/", 1)[-1].lower() + return n.startswith("zosapi") and n.endswith((".dll", ".xml", ".pdb")) + + +# ---------------------------------------------------------------------------------------------------- definitions +def example_groups(repo: Path) -> list[str]: + root = repo / "docs" / "examples" + return sorted(p.name for p in root.iterdir() if p.is_dir() and not p.name.startswith(("_", ".")) and p.name != "__pycache__") + + +def definitions(repo: Path, plugin_ids: list[str]) -> dict: + """Every component: ``kind`` (``built`` or ``source``), ``perPlatform``, ``requires``, ``description``, ``default`` (what ``install.py`` + installs without --include is nothing: ``default`` only marks the usual starting set in ``--list``), ``inAll`` (False: media), + ``sourcePaths`` (folders in git), ``subpath`` (template for a part of the component), ``pip`` (wheel installation).""" + d: dict[str, dict] = { + "cli": dict(kind="built", perPlatform=True, description="the ultrafast command-line program (self-contained)"), + "python": dict(kind="built", perPlatform=False, description="Python package (hosted engine wheel; needs the .NET 10 runtime)", + pip=dict(folder="python", distribution="ultrafast-engine", spec="ultrafast-engine[hosted]")), + "python-native": dict(kind="built", perPlatform=True, description="Python package with the NativeAOT engine (no plugins)", + conflicts=["python"], provides=["python"], inAll=False, + pip=dict(folder="python", distribution="ultrafast-engine", spec="ultrafast-engine")), + "cpp": dict(kind="built", perPlatform=True, description="C API: header, native library, CMake example"), + "csharp": dict(kind="built", perPlatform=False, description="UltrafastEngine.Core.dll for net10.0 and net48"), + "zemax": dict(kind="built", perPlatform=True, description="OpticStudio extension (Windows x64; built against your ZOS-API, which is not included)"), + "schemas": dict(kind="source", description="JSON schemas of .ufs and run.json", sourcePaths=["schemas"]), + "docs": dict(kind="source", description="documentation (Markdown, no examples / papers / design notes)", + sourcePaths=sorted("docs/" + p.name for p in (repo / "docs").iterdir() if p.is_dir() and p.name not in ("examples", "papers", "design", "__pycache__"))), + "examples.shared": dict(kind="source", description="helpers shared by all examples (_figures.py, make_all.py, README)", sourcePaths=["docs/examples"], + gitViaParents=True), + "media": dict(kind="source", description="figures and animations of examples and papers (png, gif, svg)", inAll=False, sourcePaths=["docs/examples", "docs/papers"]), + "tests": dict(kind="source", description="test sources and data (C# and Python), no binaries", sourcePaths=["tests/UltrafastEngine.Tests", "python/tests"]), + "component-tests": dict(kind="source", description="component checks with plots (tests/components)", sourcePaths=["tests/components"]), + "papers": dict(kind="source", description="paper replications (docs/papers)", sourcePaths=["docs/papers"]), + "source": dict(kind="source", description="the whole repository (tracked files)", sourcePaths=["."], inAll=False), + } + d["tests"]["requires"] = ["python"] + d["component-tests"]["requires"] = ["python"] + for g in example_groups(repo): + d["examples." + g] = dict(kind="source", description="examples: " + g, requires=["examples.shared", "python"], + sourcePaths=["docs/examples/" + g], subpath="docs/examples/%s/{rest}" % g) + for pid in plugin_ids: + d["plugins." + pid] = dict(kind="built", perPlatform=False, description="plugin " + pid, requires=["cli"] + (["plugins.fitting"] if pid == "twin" else [])) + d["python"]["default"] = d["cli"]["default"] = True + for v in d.values(): + v.setdefault("requires", []) + v.setdefault("default", False) + return d + + +# ---------------------------------------------------------------------------------------------------- source files +def tracked_files(repo: Path) -> list[str]: + """Repository-relative POSIX paths of the files git tracks (a plain walk, minus build folders, when there is no git).""" + try: + out = subprocess.run(["git", "-C", str(repo), "ls-files", "-z"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True).stdout + return [p for p in out.decode("utf-8").split("\0") if p and (repo / p).is_file()] + except (OSError, subprocess.CalledProcessError): + res = [] + for p in sorted(repo.rglob("*")): + rel = p.relative_to(repo) + if p.is_file() and not any(part in SKIP_PARTS for part in rel.parts): + res.append(rel.as_posix()) + return res + + +def _is_media(rel: str) -> bool: + return rel.lower().endswith(MEDIA_EXT) and rel.startswith(MEDIA_ROOTS) + + +def _skip(rel: str, name: str) -> bool: + """Tracked files are already clean. Never packaged: ZOS-API assemblies, licensed paper PDFs; the agent notes (CLAUDE.md) except in ``source``.""" + if forbidden(rel) or (rel.lower().endswith(".pdf") and rel.startswith("docs/papers/")): + return True + return name != "source" and rel.rsplit("/", 1)[-1] in SKIP_NAMES + + +def collect(repo: Path, name: str, defs: dict, files: list[str] | None = None) -> list[tuple[str, Path]]: + """The (archive path, file) pairs of a source component; ``files`` = ``tracked_files(repo)`` (pass it when collecting many).""" + files = tracked_files(repo) if files is None else files + roots = defs[name]["sourcePaths"] + out = [] + for rel in files: + if _skip(rel, name): + continue + media = _is_media(rel) + if name == "media": + ok = media + elif name == "source": + ok = True + else: + if media: + continue + if name == "examples.shared": + parts = rel.split("/") + ok = rel.startswith("docs/examples/") and len(parts) == 3 # files directly in docs/examples + elif name == "docs": + ok = any(rel.startswith(r + "/") for r in roots) or (rel.count("/") == 1 and rel.startswith("docs/")) or rel in ("mkdocs.yml", "llms.txt") + ok = ok and not rel.endswith(".py") + else: + ok = any(rel == r or rel.startswith(r.rstrip("/") + "/") for r in roots) + if ok: + out.append((rel, repo / rel)) + return out + + +# ---------------------------------------------------------------------------------------------------- assets +def write_asset(files: list[tuple[str, Path]], out_file: Path) -> dict: + """A reproducible ``.tar.gz`` (sorted, fixed mtime, no owner names). Refuses ZOS-API files. Returns file / size / sha256.""" + bad = [a for a, _ in files if forbidden(a)] + if bad: + raise SystemExit("refusing to package ZOS-API files (Ansys property): " + ", ".join(bad)) + out_file.parent.mkdir(parents=True, exist_ok=True) + with open(out_file, "wb") as raw, gzip.GzipFile(fileobj=raw, mode="wb", mtime=0, compresslevel=9) as gz, tarfile.open(fileobj=gz, mode="w", format=tarfile.PAX_FORMAT) as tf: + for arc, path in sorted(files, key=lambda t: t[0]): + ti = tarfile.TarInfo(arc) + data = path.read_bytes() + ti.size = len(data) + ti.mtime = 0 + ti.mode = 0o755 if (path.stat().st_mode & 0o111) else 0o644 + ti.uid = ti.gid = 0 + ti.uname = ti.gname = "" + tf.addfile(ti, io.BytesIO(data)) + h = hashlib.sha256(out_file.read_bytes()).hexdigest() + return {"file": out_file.name, "size": out_file.stat().st_size, "sha256": h} + + +def asset_name(component: str, version: str, platform: str) -> str: + return "%s-%s%s.tar.gz" % (component, version, "" if platform == "any" else "-" + platform) + + +def check_archives(paths: list[Path]) -> list[str]: + """Names of ZOS-API files found in the given archives (.tar.gz, .zip, .whl) or folders: the release gate.""" + import zipfile + found = [] + expanded = [] + for p in paths: + expanded.append(p) + if p.is_dir(): # a folder: its loose files and every archive in it + expanded += [f for f in sorted(p.rglob("*")) if f.is_file() and (f.name.endswith((".tar.gz", ".whl", ".zip")))] + for p in expanded: + if p.is_dir(): + found += [str(f) for f in p.rglob("*") if f.is_file() and forbidden(f.name)] + elif p.suffix in (".zip", ".whl"): + found += ["%s:%s" % (p.name, n) for n in zipfile.ZipFile(p).namelist() if forbidden(n)] + else: + with tarfile.open(p, "r:*") as tf: + found += ["%s:%s" % (p.name, n) for n in tf.getnames() if forbidden(n)] + for m in tf.getmembers(): # wheels inside archives + if m.name.endswith(".whl") and m.isfile(): + try: + names = zipfile.ZipFile(io.BytesIO(tf.extractfile(m).read())).namelist() # type: ignore[union-attr] + except zipfile.BadZipFile: + raise SystemExit("%s:%s is not a valid wheel" % (p.name, m.name)) + found += ["%s:%s:%s" % (p.name, m.name, n) for n in names if forbidden(n)] + return found + + +# ---------------------------------------------------------------------------------------------------- manifest +def manifest(version: str, assets: dict[str, list[dict]], defs: dict, repository: str = "https://github.com/JosephCodemard/UltrafastEngine") -> dict: + """``assets``: component -> [{platform, file, size, sha256}]. Components without an asset but with children are listed as virtual + parents (no files); the installer expands them. ``defs`` entries are copied without the build-only keys.""" + comps = {} + for name, d in defs.items(): + entry = {k: d[k] for k in ("description", "requires", "default", "conflicts", "pip", "subpath", "sourcePaths", "gitViaParents", "provides") if d.get(k)} + entry["default"] = bool(d.get("default")) + if d.get("inAll") is False: + entry["inAll"] = False + entry["requires"] = list(d.get("requires", [])) + entry["assets"] = sorted(assets.get(name, []), key=lambda a: a["platform"]) + if entry["assets"] or d.get("sourcePaths"): + comps[name] = entry + return {"schema": 1, "product": "UltrafastEngine", "version": version, "tag": "v" + version, "repository": repository, "components": comps} + + +def merge(manifests: list[dict]) -> dict: + """Combine the manifests of several build jobs (each built other components / platforms) into one; the versions must agree.""" + if not manifests: + raise SystemExit("nothing to merge") + out = json.loads(json.dumps(manifests[0])) + for m in manifests[1:]: + if m["version"] != out["version"]: + raise SystemExit("version mismatch: %s and %s" % (out["version"], m["version"])) + for n, c in m["components"].items(): + t = out["components"].setdefault(n, {**c, "assets": []}) + have = {a["platform"] for a in t["assets"]} + for a in c["assets"]: + if a["platform"] in have: + old = next(x for x in t["assets"] if x["platform"] == a["platform"]) + if old["sha256"] != a["sha256"]: + raise SystemExit("%s for %s was built twice with different content" % (n, a["platform"])) + continue + t["assets"].append(a) + t["assets"].sort(key=lambda a: a["platform"]) + return out + + +def main(argv=None) -> int: + argv = sys.argv[1:] if argv is None else argv + if argv[:1] == ["merge"] and len(argv) >= 3: + m = merge([json.loads(Path(p).read_text(encoding="utf-8")) for p in argv[2:]]) + Path(argv[1]).write_text(json.dumps(m, indent=1), encoding="utf-8") + print("merged %d manifests -> %s (%d components)" % (len(argv) - 2, argv[1], len(m["components"]))) + return 0 + if argv[:1] == ["check-zosapi"] and len(argv) >= 2: + paths = [] + for a in argv[1:]: + p = Path(a) + paths.append(p) + found = check_archives(paths) + for f in found: + print("ZOS-API file in release: " + f) + print("ZOS-API check: %s" % ("FAILED" if found else "clean")) + return 1 if found else 0 + print(__doc__) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/architecture/code-map.md b/docs/architecture/code-map.md index 498c996..31b69f5 100644 --- a/docs/architecture/code-map.md +++ b/docs/architecture/code-map.md @@ -31,7 +31,9 @@ Paths are under `src/UltrafastEngine.Core/` unless stated. Line counts change; u | `Engine/Compiler.cs` | `Compiler`, `CompiledRun`, `CompiledElement`, `CompilerServices` | each element to linear terms, a nonlinear segment or an active element. Beam tracking | | `Engine/CompilerComponents.cs`, `CompilerFiber.cs`, `CompilerSubstrate.cs` | | plates, polarisers, prisms; fibres; mirror substrates | | `Engine/GridPlanner.cs` | `GridPlanner` | the time grid a run needs | -| `Optics/BeamTracking.cs` | `BeamState` | Gaussian q, ray matrices and a five-ray bundle through the system | +| `Materials/Catalogue.cs`, `CatalogueModels.cs`, `CatalogueReference.cs` | `Catalogue`, `CatalogueEntry`, `CatalogueQuantity`, `CatalogueStatus` | the material catalogue (kinds solid, gas, raman, gain, crystal): loads the embedded files of `Data/materials/` (+ the user's `~/.ultrafast/materials`), builds index / Raman objects from them, writes `docs/reference/materials-catalogue.md`. `MaterialLibrary`, `GasData`, the Raman models, `GainMedium.Get`, `CrystalLibrary.Get` and `CiddorAirIndex` read their numbers here | +| `Optics/BeamTracking.cs` | `BeamState` | Gaussian q, ray matrices and a five-ray bundle through the system; `Duct` / `DuctMatrix` (graded-index duct, the thermal lens of a gain slice), `ThinPowerForEffectiveFocalLength` | +| `Physics/DispersiveResponses.cs` | `DispersiveTerm`, `DispersiveResponses` | the frequency-resolved part of a time-domain response (the Drude term of `PlasmaResponse`, `IDispersiveResponse`): evaluated at the carrier in time, applied as `i (omega/c) R(omega) FFT[dn A]` by the envelope, carrier-resolved and spatial propagators | ## Execute @@ -52,12 +54,12 @@ Paths are under `src/UltrafastEngine.Core/` unless stated. Line counts change; u | folder | what it holds | |---|---| -| `Physics/` | `LinearTerms.cs` (`LinearTerm`, `PhaseTerm`, `GroupDelayTerm`, `JonesTerm`, `DispersionPoint`); `Nonlinear.cs` (`EnvelopeProblem`, `FieldProblem`, steppers); ionisation | +| `Physics/` | `LinearTerms.cs` (`LinearTerm`, `PhaseTerm`, `GroupDelayTerm`, `JonesTerm`, `DispersionPoint`); `Nonlinear.cs` (`EnvelopeProblem`, `FieldProblem`, steppers); `DispersiveResponses.cs`; `Ionization.cs` (rates, `PlasmaResponse` with its Drude operator) | | `Transverse/` | `SpatialPropagation.cs` (the spatial nonlinear solver; `ExecuteLinear` streams the stored frequencies in blocks, `transverse.frequencyBlock`); `TransverseBasis.cs` (`RadialBasis`, `CartesianBasis`); `TransverseProfile.cs` (`TransverseProfile`, `TransverseOps`: expand, collapse, regrid, `Curvature`, `BeamQuality`); `BeamShapes.cs` (initial profiles) | | `Optics/` | `BeamTracking.cs`; `DispersiveSurface.cs` (gratings, prisms); `SurfaceOptics.cs` (lenses, curved mirrors); `MaskElement.cs`; `ModulatorElement.cs` (pixelated phase / amplitude device: cell grid, fill factor, quantisation, cross-talk, chromatic rules `opd` / `fixedPhase` / `index`, `PhaseScale`); `ComponentTerms.cs` (Zernike, figure); `MirrorSubstrate.cs`, `DelayedTerms.cs` | | `Crystals/` | χ(2): `CrystalElement`, phase matching | -| `Gain/`, `Lasers/` | amplifiers; `Oscillator.cs`, `Resonator.cs`, saturable absorbers; `CellMode.cs` (`CellModeSolver.Solve`: Kerr-matched mode of a two-mirror multi-pass cell, command `cellMode`) | -| `Materials/` | index models, the library, Raman | +| `Gain/`, `Lasers/` | amplifiers; `AseModel.cs` (`AseSpec`, `AseModel`: inter-pulse ASE, both directions, per transverse cell and frequency, at frozen N2; `Rates` feeds `GainCrystal.PumpCw` / `Decay`, `ModeOutput` gives the ASE in the signal mode); `GainElements.ApplyStressPattern` (the four-lobed Jones map); `Oscillator.cs`, `Resonator.cs`, saturable absorbers; `CellMode.cs` (`CellModeSolver.Solve`: Kerr-matched mode of a two-mirror multi-pass cell, command `cellMode`) | +| `Materials/` | index models, the library, Raman; the catalogue (`Catalogue.cs`) with the numbers and their sources (`Data/materials/`) | | `Instruments/` | `Instruments.cs` (the registry), `ActiveInstruments.cs` (`beamProfile`, ...), `CameraInstruments.cs`, `Detector.cs`, `Sampling.cs`, `SingleShotCorrelator.cs` (`singleShotCorrelator`, `SingleShotGeometry.Solve`: time-to-space correlator) | | `Measured/` | `Measurements.cs`: comparison of instruments with recorded data | | `Retrieval/` | FROG, d-scan, Gerchberg-Saxton | @@ -72,5 +74,8 @@ Paths are under `src/UltrafastEngine.Core/` unless stated. Line counts change; u | `python/tests/` | the Python package; `test_pynlo*.py` and `test_lightpipes.py` are cross-checks with independent codes, skipped when those are absent | | `build/example_check.py` | runs every example and compares the numbers quoted in its README | | `build/gallery.py` | writes `docs/gallery.md` from the `hero.png` / `animation.gif` files next to the examples | +| `install.py` | the selective installer / uninstaller of a release: `fetch_manifest`, `expand` / `resolve` (dotted names, dependencies, platform assets), `install`, `uninstall`, `install_git`; the install record `.ultrafast-install.json` | +| `build/release_components.py` | the component registry (`definitions`), source file selection from `git ls-files` (`collect`), the reproducible asset writer, manifest build and merge, the ZOS-API gate (`forbidden`, `check_archives`) | +| `build/package.py` (`--components`) | `build_release` slices what the single-folder `Builder` steps produce into per-component assets; `.github/workflows/release.yml` runs it per OS and merges | | `build/examples_cli.py` | the `ultrafast-examples` command (`list`, `copy`, `run`); `build/package.py --examples-wheel` installs it with the examples as package data | | `docs/examples/make_all.py`, `_figures.py`, `//make_figures.py` | run every example and write its figures (`figures/`, git-ignored) or, for the Computing group, the committed `hero.png` and `animation.gif`; checks the size budget | diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 9530c92..e1f12dc 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -7,7 +7,7 @@ How the engine is built, for anyone changing it. [Concepts](../concepts.md) expl | [Code map](code-map.md) | the folders, the main files and classes, and what each does | | [Key processes](processes.md) | what happens in a run, step by step: compile, the graph, execution, grids, studies | -Why the branched design took its form is in [Design: branched systems](../design/branching.md). +Why the branched design took its form is in [Design: branched systems](../reference/branches.md). The layers: diff --git a/docs/architecture/processes.md b/docs/architecture/processes.md index 142b9bc..ebb53e5 100644 --- a/docs/architecture/processes.md +++ b/docs/architecture/processes.md @@ -20,6 +20,16 @@ In `Compiler.Compile`, in order: 6. `BuildGridAndPulse`: plan the grid and build the input pulse. 7. Pick the field mode of each nonlinear segment. +## Material numbers: from a catalogue file to a run note + +1. `Catalogue` (static, lazy) loads the embedded `Data/materials/**/*.json`, then the user's files (`$ULTRAFAST_MATERIALS`, default `~/.ultrafast/materials`); a user quantity replaces the built-in one and adds a notice. +2. `MaterialLibrary.BuiltIn(name)` finds the entry (aliases included) and builds a `Material` from its quantities (`CatalogueModels.Index`, `.Raman`); the quantities it used are kept in `Material.Used`. + An override (`MaterialLibrary.Apply`: `n2`, `index`, `raman`, ...) removes the quantity from `Used`. +3. The compiler, where an element really uses a property (`CompileBulk`: the dispersion, and for a nonlinear element n2, the Raman response, the Kerr anisotropy, the rotational Raman data, the ionisation data), calls + `ProvenanceLog.UseCatalogue`. That puts the status and source into `provenance.json` and, for a value that is not `verified` (or comes from a user file), once into the run notes and `ProvenanceLog.UnverifiedInputs`. +4. `RunResult.SummaryJson` writes the list as `unverifiedInputs`. Gain media (`GainMedium.Used`, reported in `GainCompiler`) and crystals (`CrystalModel.Used`, in `CrystalElement.Compile`) follow the same path. +5. At the end of `Compiler.Compile`, `physics.strictMaterials` (or `ULTRAFAST_STRICT_MATERIALS=1`) turns every non-verified entry of `UnverifiedInputs` into one `ModelException`. + ## Execute: one element `Executor.RunElement(state, index, element, pulse, path)`, for a pulse on one path: @@ -31,6 +41,14 @@ In `Compiler.Compile`, in order: A Sequential system calls this for each element in turn, in the loop in `Executor.Execute`. +## A pumped gain element and its ASE + +`GainElement.Execute` / `AmplifierElement.Execute` build a `GainCrystal` (N2 per slice and transverse cell), optionally attach an `AseModel` (`aseModel`), then +reach the state the pulse sees: `GainRun.Train` iterates the pulse train with the energy model (`FastPass`), each interval through `GainRun.Between` -> `PumpCw` / `Decay`. With an `AseModel` +every pumping or decay step asks `AseModel.Rates(N2, N_t)`: per cell and frequency, forward and backward intensity through the slices at frozen N2 (exact slice step), the stimulated photons of each slice divided +by N2 are an extra decay rate of that slice (sub-stepped so the rate stays small within a step). `GainRun.RecordAse` then evaluates `ModeOutput` (the ASE in the signal mode) and the element reports the pedestal +strength. The pulse itself is amplified by `Pass1D` / `PassSpatial` as before; a stress map (`ApplyStressPattern`) acts on the spatial field after the gain. + ## Execute: a Branched system `Executor.RunGraph`: diff --git a/docs/capabilities.md b/docs/capabilities.md index 66b651c..128e98f 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -126,9 +126,10 @@ the same way ([Plugin system](reference/plugins.md)). **Physics** * Beam tracking handles simple astigmatism (principal axes x and y). The tracked beam stays Gaussian: a thermal lens is - applied as the parabola that fits it best over the beam (its aberration needs a spatial model), and the dispersion of - spatio-temporal couplings is applied to fourth order (GDD, TOD, FOD). A multipass amplifier puts its thermal lens on the - tracked beam only when given the path between passes; a regenerative amplifier does not. + applied as a graded-index duct per slice, the parabola that fits it best over the beam (its aberration and gain narrowing need a spatial + model), and the dispersion of spatio-temporal couplings is applied to fourth order (GDD, TOD, FOD). A multipass amplifier puts its thermal lens on the + tracked beam only when given the path between passes; a regenerative amplifier the same way, with an integer `roundTrips` and the free path + between passes (the cavity's own optics are not part of the tracked beam). * One-dimensional elements do not change the beam shape: self-focusing above the critical power and aberration effects need a spatial model (the engine warns). Carried profiles take angular dispersion only with `physics.spatioTemporal` = `"profile"`, on cartesian grids. @@ -140,7 +141,7 @@ the same way ([Plugin system](reference/plugins.md)). no hollow-capillary (Marcatili) waveguide model. * The carrier-resolved field mode is scalar (one polarisation; a y component is propagated linearly with a warning). * ASE in amplifiers is one-dimensional, in the signal polarisation, and covers the simulated time window only. -* In envelope mode, the Drude plasma term uses the carrier frequency. +* The Drude plasma index is applied frequency by frequency in all three models (envelope, carrier-resolved, spatial) as the first-order expansion in the density; a density that changes within an optical cycle is resolved in the ionisation rate, not in the Drude response. **Numerics and models** diff --git a/docs/claude-code.md b/docs/claude-code.md index e377847..a42e8e3 100644 --- a/docs/claude-code.md +++ b/docs/claude-code.md @@ -16,7 +16,7 @@ Then ask in plain words, or use the project commands: | command | what it does | |---|---| | `/test NerTests Measured` | builds the tests and runs the matching ones (empty: all, incl. slow); shows failures only | -| `/regen` | regenerates the 5 generated files (schemas, options.md, Python types and API docs) after a schema change | +| `/regen` | regenerates the 6 generated files (schemas, options.md, materials-catalogue.md, Python types and API docs) after a schema change | | `/verify` | full check before a commit or release: build, C# (.NET 10 and .NET Framework 4.8), Python with pyNLO, examples in strict mode, docs | | `/physics-change ` | workflow for a change to the propagation physics: equation and source first, test against an analytic limit, all three propagation models kept consistent, review | | `/new-option ` | adds an option / element / instrument / model kind end to end | diff --git a/docs/concepts.md b/docs/concepts.md index 52f74d0..ac477fb 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -43,7 +43,7 @@ an execution plan: 2. turn each element into *linear terms* (dispersion, Fresnel/coating, clip, scatter, absorption, filter, polarisation optics...) and, where the medium is nonlinear and a beam size is known, a *nonlinear segment*; 3. size the time/frequency grid from the pulse and the largest group-delay spread anywhere in the system - (and the expected nonlinear broadening), put the grid centre on a bin of the frequency lattice (within half a bin of the carrier) and build the input pulse at the requested carrier itself (an offset of the lattice), so the spectral centre, the Kerr phase n2 ω/c and the angles of gratings and prisms all refer to the wavelength you gave. A pulse at the requested carrier travels with the group delay at that carrier, which differs from the lattice centre's (the frame's) by GDD x offset: a sub-sample drift of the pulse in the window. + (and the expected nonlinear broadening), put the grid centre on a bin of the frequency lattice (within half a bin of the carrier) and build the input pulse at the requested carrier itself (an offset of the lattice), so the spectral centre, the Kerr phase n2 ω/c and the angles of gratings and prisms all refer to the wavelength you gave. A **cw source** is a single line, which cannot be an offset of the lattice: its grid is shifted instead, so that a bin sits exactly on the requested carrier (`snapCarrier: true` written in the spec restores the snapped lattice, which carrier-resolved models need). A pulse at the requested carrier travels with the group delay at that carrier, which differs from the lattice centre's (the frame's) by GDD x offset: a sub-sample drift of the pulse in the window. It then **executes**: linear terms are applied exactly (one complex multiplication per frequency, any length), each with its own energy accounting; nonlinear segments are integrated in the interaction picture with the diff --git a/docs/css/brand.css b/docs/css/brand.css index c5a13e4..adebabe 100644 --- a/docs/css/brand.css +++ b/docs/css/brand.css @@ -1,11 +1,17 @@ -/* UltrafastEngine brand colours (the logo palette in assets/logo/make_logo.py). The readthedocs theme is light only. */ -:root { --uf-blue: #2a78d6; --uf-blue-dark: #184f95; --uf-ink: #0b0b0b; } -.wy-side-nav-search, .wy-nav-top { background: var(--uf-blue-dark); } +/* UltrafastEngine brand colours (the logo palette in assets/logo/make_logo.py): navy ink, logo blue, logo orange. The readthedocs theme is light only. */ +:root { --uf-navy: #18202c; --uf-blue: #336eb0; --uf-blue-dark: #214b8d; --uf-orange: #c85a28; --uf-orange-light: #ea8b41; --uf-ink: #18202c; } +.wy-side-nav-search, .wy-nav-top { background: var(--uf-navy); } +.wy-nav-side { background: #222c3b; } .wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a { color: #fcfcfb; } .wy-side-nav-search > a img.logo { width: 100%; max-width: 220px; margin: 0.4em auto 0.6em; } -.wy-menu-vertical a:hover { background: var(--uf-blue-dark); } -.wy-menu-vertical li.current > a, .wy-menu-vertical li.on a { border-left: 3px solid var(--uf-blue); } +.wy-side-nav-search input[type=text] { border-color: var(--uf-orange); } +.wy-menu-vertical a:hover { background: var(--uf-navy); } +.wy-menu-vertical li.current > a, .wy-menu-vertical li.on a { border-left: 3px solid var(--uf-orange); } a { color: var(--uf-blue-dark); } -a:hover { color: var(--uf-blue); } +a:hover { color: var(--uf-orange); } +.rst-content h1, .rst-content h2 { border-bottom: 2px solid var(--uf-orange-light); padding-bottom: 0.15em; } .rst-content code, .rst-content tt, code { color: var(--uf-ink); } .rst-content img { max-width: 100%; height: auto; } +/* long logo at the top of every page */ +.uf-banner { display: flex; align-items: center; margin: -1.2em -1.6em 1.4em; padding: 0.7em 1.6em; background: #fcfcfb; border-bottom: 3px solid var(--uf-orange); } +.uf-banner img { height: 44px; width: auto; } diff --git a/docs/design/plan-next.md b/docs/design/backlog.md similarity index 82% rename from docs/design/plan-next.md rename to docs/design/backlog.md index e18ff2b..ce13286 100644 --- a/docs/design/plan-next.md +++ b/docs/design/backlog.md @@ -1,17 +1,16 @@ -# Plan for the next work: design plugins, waveguides, fitting, engineering, papers +# Backlog: design that is parked for after the first release -Status (2026-11-08, version 2.2.0): done are the T1 branch items, the component sheet and tests (`tests/components/`), twelve paper -replications, plots of Branched systems, the measured pedestal, the XPW model (cubic χ(3) tensor with BaF2), FROG kinds and -multi-trace retrieval, the multi-pass-cell mode helper and the single-shot correlator (see the changelog and the -[November 2026 fix plan](plan-fixes-2026-11.md)). This page keeps the design of what is left; the user-facing list is the -[roadmap](../roadmap.md) and the handover is `HANDOFF.md` in the repository root. Written 2026-11; sizes are focused agent-days -(S ≈ 1, M ≈ 2-3, L ≈ 4-6, XL ≥ 7). Zemax is out of scope here. The adaptive-optics and digital-twin work follows its own [roadmap](roadmap-ao-twin-2026-11.md); its session S1 is done (2026-11-09). +This page was the plan for the work after 2.2.0 (written 2026-11-08). The adaptive-optics and digital-twin work from it is done +([history](history.md)); the plan for the next iteration is [next-steps.md](next-steps.md). What stays here is design that is **not scheduled**: +engineering (GPU coverage, FP32, speed), multimode and tapered fibres, fitting extensions, the crystal- and coating-design plugins, papers still to replicate, +and the repetition / Pockels-cell sketch. Sizes are focused agent-days (S ≈ 1, M ≈ 2-3, L ≈ 4-6, XL ≥ 7). Zemax is out of scope here. Owner decisions 1-11 below +still hold unless next-steps.md says otherwise. ## Decisions (owner) | # | decision | |---|---| -| 1 | Push to GitHub: yes. PoCL is added to CI so `GpuTests` run there. | +| 1 | Push to GitHub: yes. PoCL is added to CI so `GpuTests` run there (not done yet). | | 2 | No PyPI publishing. The release workflow builds packages and wheels and attaches them to a GitHub release on a version tag. | | 3 | **Plugins as pip extras**: `pip install "ultrafast[hitran,cuda,fitting]"`, `[opencl]`, `[coatingdesign]`, `[crystaldesign]`, `[all]`. The default Python engine is the NativeAOT library, which can only use plugins compiled into it, so: the native library in the `ultrafast` wheel is built with every non-Zemax plugin compiled in (off until used); each extra installs that plugin's Python module (`ultrafast-hitran`, `ultrafast-fitting`, ...) and its third-party dependencies (`[cuda]`: NVIDIA's NVRTC / cuFFT wheels, not on macOS); the hosted engine loads the plugin assemblies shipped in the same extra wheels. Without PyPI, extras resolve only with `pip install "ultrafast[hitran]" --find-links `. Zemax is never an extra. | | 4 | (delegated) **FP32 study first; then mixed precision in the existing CUDA / OpenCL back-ends; Metal only if the study shows FP32 is enough for the steps that matter.** Reason: Apple GPUs have no FP64, so Metal is worthless unless FP32 works, and the study is cheap and runs on this Mac. | @@ -56,7 +55,7 @@ soliton in exponentially dispersion-decreasing fibre, T(z) = T0 e^{-z/L} (Cherni signal and pump power (co-pumped initial value, counter-pumped boundary value), g(ω,z) as a z-dependent linear term, iterated with the propagated spectrum; reuses `GainCrystal`. Yb:silica cross-sections quoted from Paschotta et al., IEEE JQE 33, 1049 (1997) (read from the paper, not recalled). Tests: closed-form two-level steady state (Saleh et al., -IEEE PTL 2, 714 (1990)) to 1e-4; exact photon balance; small-signal limit equals `gainDbPerM`. ASE out of scope. +IEEE PTL 2, 714 (1990)) to 1e-4; exact photon balance; small-signal limit equals `gainDbPerM`. ASE out of scope here; `Gain/AseModel.cs` (session D) is the shared solver: use a fibre's mode as the bundle (Ω = λ0² / (n² A), p = 2) and its Γ-weighted N2. ## D. Fitting plugin (all inside `plugins/fitting/src`, no packages: everything hand-written) @@ -123,6 +122,26 @@ Kramers-Kronig-consistent by construction, instead of an ideal `gdd` coating. | fitting showcases | lab-mpc-ner-contrast (decision 9); Song 2022 spectra; Hollenbeck & Cantrell 2002 Raman response | +## F. Repetition and Pockels cells (sketch, was "S6") + +*What the engine does today.* Most of it is single shot. Gain and amplifier elements with a `repRateHz` (on the pulse, or on the element, which then acts as an ideal +pulse picker) iterate the pump-amplify cycle to a steady state with a fast energy model, then propagate the last pulse with the full model; period doubling is detected +and warned about ([gain](../reference/gain.md)). A regenerative amplifier is `geometry: "regenerative"` with a number of round trips. `grid.tail` carries delayed light. +*Not there:* a **switch model** (a Pockels cell, a pulse picker with finite contrast, a switching window with rise time, the leakage of a regen's cavity switch into +neighbouring pulses). Time between pulses is only the repetition period of the gain model. + +*Why it matters.* The Pockels cell decides which pulse goes where (pulse picking, regen switch-in and switch-out, cavity dumping), and its imperfections are the +contrast problems the engine already cares about: finite extinction (1e-3 to 1e-2 leakage), finite rise and fall time, voltage-dependent retardance, and in a regen the +round-trip number that sets energy and pre-pulse level. Repetition also decides thermal lensing (average power), what a DM or SLM sees (averages, not single shots) and +tiled-array phase noise (a rate question). + +*Design sketch.* (1) A `pockelsCell` element: a voltage-controlled waveplate pair with a polariser, crystal (KD*P, BBO, RTP, LN) with V_pi, length, extinction ratio and a +voltage waveform V(t) (rise, flat top, fall, delay), built from the existing Jones elements. (2) A pulse-train layer: N pulses at the repetition period, each with +transmission T(V(t_k)), the full propagation for the pulse that matters, a macro-time table of energy and pre- and post-pulse leakage per pulse. (3) A regen switch with +switch-in and switch-out in round trips and the cell's edge times, so that the pre-pulse and the cavity-dump contrast come out of the model. (4) Examples: a pulse-picker +contrast and a regen with switching. Open for the owner: which switch first (picker, regen, cavity dump), and whether switch contrast is something to fit with the twin. + + ## Rules for work on these items One working branch per item (`dev/...`), merged to `main` after `/verify` and a green CI. Shared files (changelog, status, roadmap pair, validation matrix) are updated by whoever merges. Hot files kept sequential: `Nonlinear.cs` (speed work, then waveguides, then GPU coverage), `CrystalElement.cs` (crystal design tier 2), `BuiltInElements.cs` (regenerate after each merge), `Gain/` (rate equations only after the waveguide work). diff --git a/docs/design/branching.md b/docs/design/branching.md deleted file mode 100644 index 9023a21..0000000 --- a/docs/design/branching.md +++ /dev/null @@ -1,386 +0,0 @@ -# Design: branched systems - -Status: **built** (stages 1 to 8, 2026-10-30; reviewed 2026-10-31). This page records the design and why it took this form. Where the plan and the code differ, the code and [what changed in building it](#15-what-changed-in-building-it) are right. User reference: [Branched systems](../reference/branches.md). Code: [Architecture](../architecture/index.md). -Background: auxiliary beams were built as "ports with a pluggable source, so a later branch layer only connects ports". This design is that layer. - -## 1. The plan in brief - -* **Every element has declared input and output ports**, in the schema, for all element types. -* **Two ways to write a system.** `Sequential([...])` is today's list. `Branched([...])` requires every element to say where its light goes, with `next`, naming the target's input port (`"BS1:1"`, or `{"id": "BS1", "port": 1}`). A link back makes a loop. -* **Internally every system is a graph.** A separate compile step builds it and verifies it. That step, not a different executor, is the difference between sequential and branched systems. -* **Virtual ports.** Any input port can be fed by a beam that belongs to no beamline: a `pulse`, or a result file. Today's `aux` beams are exactly this. Any output port can be chosen to continue, so a beam splitter works in a plain Sequential system. In a Branched system an unused port raises an alert and gets a virtual input or output. -* **Loops are in the design from the start**: the graph compiler handles them from its first version, and they run in the stage straight after acyclic graphs. They cannot come sooner, because a loop needs combining and packets. -* **Phase is the hard part.** The engine discards each element's carrier phase. A per-path ledger has to keep it ([section 6](#6-the-path-ledger-phase-and-time)). -* **Modelled instruments** become examples compared with the ideal instruments ([section 10](#10-modelled-instruments)). - -## 2. Writing a system: `Sequential` and `Branched` - -There are two ways to write a system, and one graph underneath. - -**Sequential** is today's list: entries run in file order. `"output"` chooses the port that continues. Unused ports are normal here (a crystal's by-products), and are recorded and booked. - -**Branched** is a list in which **every element states where its light goes**. There is no fall-through. In a file, `"topology": "branched"`; in Python, `Branched([...])`. The list order has no optical meaning, except that the first entry takes the run's input. - -`next` is a string or a dict, and it names the *input port* of the target: - -```jsonc -"next": "BS1:1" // element BS1, input port 1 -"next": { "id": "BS1", "port": 1 } // the same -"next": "BS1" // the default input port -"next": "BS1:b" // ports also have names -"next": "end" // the system's output -"next": { "c": "armA", "d": "bs2:2" } // several outputs: one target per output port (name or number) -``` - -Port numbers start at 1 and follow the order declared for the element type, so port 1 of a splitter's inputs is `a`. A dict with the key `id` is one target; any other dict is keyed by output port. - -A Mach-Zehnder: - -```jsonc -{ "format": "ultrafast-system", "version": 3, "name": "mach-zehnder", "topology": "branched", - "elements": [ - { "id": "bs1", "type": "beamSplitter", "reflectivity": 0.5, "next": { "c": "armA", "d": "armB" } }, - { "id": "armA", "type": "gap", "lengthM": 0.2, "next": "bs2:1" }, - { "id": "armB", "type": "gap", "lengthM": 0.2, "next": "bs2:2" }, - { "id": "bs2", "type": "beamSplitter", "reflectivity": 0.5, "next": { "c": "end", "d": "virtual" } } - ] } -``` - -A ring with one output coupler, where the jump to `oc:2` closes the loop: - -```jsonc -"elements": [ - { "id": "oc", "type": "beamSplitter", "reflectivity": 0.9, "next": { "c": "leg", "d": "end" }, - "loop": { "kind": "ringDown", "maxRoundTrips": 200, "tolerance": 1e-6 } }, - { "id": "leg", "type": "gap", "lengthM": 0.01, "material": "Vacuum", "next": "oc:2" } -] -``` - -### Unused ports are alerted, and become virtual - -In a Branched system, **every port of every element is accounted for**. The compiler raises a warning for each port nothing is connected to, and adds a virtual port so the system still runs: - -| unused port | what the compiler adds | what it means | -|---|---|---| -| input | a virtual input with no beam | vacuum: no light enters there | -| output | a virtual output | the light is recorded (`at: "id:port"`) and booked in the budget | - -To say a port is unused on purpose, and silence the warning, declare it: `"next": { "d": "virtual" }` for an output, `"inputs": { "b": { "virtual": true } }` for an input. A virtual input may also hold a beam, exactly as in a Sequential system ([section 4](#virtual-inputs)). - -**Mis-wiring is caught.** A port takes one source, so two arms pointed at one input is a compile error: beams only meet at an element with two input ports. A forgotten `next` gives a warning naming the element and port. - -| alternative | verdict | -|---|---| -| fall-through inside Branched, as in the previous revision | dropped: the owner prefers every link stated, which reads the same in any order | -| `connections` list, first draft | dropped: it moves the structure away from the elements | -| inferring the topology from the presence of `next` | dropped: a stated `topology` lets the compiler require a complete description | - -Ids in study paths such as `element:armB/lengthM` do not change. Files without `topology` are Sequential, so no migration is needed. `id#port` keeps working in `at`, and `id:port` is accepted there too. - -## 3. Compile and verify - -Sequential and branched systems differ at this step, as the owner suggested, and only in how much it has to do. - -```text -system file -> GraphCompiler -> SystemGraph -> segments, loops -> Compiler (as today) -> scheduler - ports, jumps nodes, edges cut at multi-port - verification canonical form nodes, cycles -``` - -`SystemGraph` is a plain data structure and the only input the rest of the engine sees. For a chain it has one segment, and verification finds nothing to do. - -| check | result | -|---|---| -| unknown id or port; a target that is an output | error | -| an input port with two sources; a required port with none | error | -| unreachable element | error, naming it | -| loop with no `loop` settings, or with no way out, or with zero path length | error | -| two sources meeting that are not coherent, with no declared relation | warning: intensities add | -| two arms at different carriers meeting | error, unless they share a wide enough grid | -| window needed for arm delays and echoes, against `grid.maxN` | error, or packets ([section 6](#6-the-path-ledger-phase-and-time)) | -| a nonlinear element in a loop whose round trip is shorter than the pulse | warning: passes overlap in that element | -| net gain above 1 around a loop | warning for a `ringDown` | -| shared gain state (`stateId`) reached in an order that depends on arrival times | error when the order is ambiguous | -| a port nothing is connected to (Branched) | warning; a virtual port is added | -| a Branched element with no `next` | warning; a virtual output is added | - -`ultrafast validate` already exists and gains all of this without running a simulation. New: `ultrafast validate system.ufs --graph` prints the nodes, edges, paths, loops, and each arm's delay and phase, as text or as a Mermaid diagram for the docs. The same output goes into `summary.json` as `graph`. - -## 4. Ports on every element - -`ComponentSpec` gains a declaration: - -```csharp -.Ports(inputs: new[] { "a", "b" }, outputs: new[] { "c", "d" }, defaultIn: "a", defaultOut: "c") -``` - -The default for every element type is `in` and `out`, so only multi-port types declare anything. Ports appear in the schemas, `options.md`, and the Python types. Multi-port behaviour is a new interface beside `IActiveElement`: - -```csharp -IReadOnlyDictionary Execute(ActiveContext ctx, IReadOnlyDictionary inputs); -``` - -| element | inputs | outputs | -|---|---|---| -| almost all | `in` | `out` | -| crystal | `in` and its aux roles (`pump`, `b`, ...) | its waves (`signal`, `idler`, `sum`, ...) | -| `thinFilmPolarizer` | `a`, `b` | `transmitted`, `reflected` | -| `beamSplitter` and variants | `a`, `b` | `c`, `d` | -| amplifier, later | `in`, `pump` | `out` | - -A 2×2 splitter uses neutral names: c = t a + r' b and d = r a + t' b. `transmitted` and `reflected` are accepted aliases of `c` and `d`, correct for light entering at `a`. The ring above shows why neutral names are needed: light from `b` *reflects* to `c`. - -### Virtual inputs - -An input port with no beamline feeding it can hold a beam described in place. This works in sequential and branched systems alike: - -```jsonc -{ "id": "bs", "type": "beamSplitter", "reflectivity": 0.5, "output": "d", - "inputs": { "b": { "pulse": { "shape": "gaussian", "centerWavelengthM": 1030e-9, "durationFwhmS": 100e-15, "energyJ": 1e-6 }, - "delayS": 100e-15, "phaseRad": 0, "coherentWith": "input" } } } -``` - -* The fields are those of `aux`: `pulse` or `from`, `beam`, `delayS`. -* **`aux` becomes a spelling of `inputs`**, with role as the port name. It stays valid forever and needs no migration, and its code path becomes the virtual-input path. -* `delayS` and `phaseRad` belong to virtual inputs only. A port fed by a beamline takes its timing from the path, so `delayS` there is an error rather than a silent offset. A `delay` element in the arm does the job. -* A virtual beam is its own source, so it adds in intensity with the main beam unless `coherentWith` says otherwise. -* The rule is the same everywhere: **a real beam, a virtual beam and an empty virtual port are three states of one port.** Replacing a virtual input with a link is how a Sequential sketch grows into a Branched model. - -## 5. Execution - -```text -Scheduler order, copies at splits, loops, packets - | -Segment runner today's Executor loop, moved into a method: an unbroken chain of elements - | -Elements, solvers unchanged: linear terms, NonlinearSegment, IActiveElement, instruments -``` - -There is **one engine**. A separate executor would mean every physics change is made and tested twice. A chain is one segment and the scheduler does nothing. - -| | sequential | branched | -|---|---|---| -| Speed per element | same code | same code | -| Overhead | one check at the start | a spectrum copy per split, a sum at each meeting | -| Time window | one path | must hold relative delays, or packets are used | -| Work done | one path | every arm and pass: the physics asked for | -| Studies | prefix cache | better: only what is downstream of the change reruns | - -So sequential is not faster per element. A system that is really a chain takes the same time, and stage 1 has a benchmark to hold that. The advantage of a chain is that it is simpler to write and read. - -The order is topological, with ties in file order so results repeat. A field is released after its last consumer unless an instrument asks for it. Arms do not run in parallel at first: the solvers already use threads. - -### What the existing code has to change - -| where | today | change | -|---|---|---| -| `Executor.Execute` | one `pulse`; `arrival`, `cumLen`, `cumB`, `system` as locals | loop body becomes the segment runner; locals become `PathLedger` | -| `PhaseTerm.FrameReference` | subtracts φ₀ and τ₀; `arrival` keeps τ₀, **φ₀ is lost** | adds them to the ledger | -| `Compiler` `c.Track` | one mutable `BeamState` | set per segment to a clone of the state on its input edge; a loop iterates it | -| `PlanDownstreamGrid`, `SatelliteRoom` | `Skip(index)` | elements reachable in the graph | -| `AuxBeamSpec` | an element's own list | the virtual-input source | -| `Byproducts`, `res.At["id#field"]` | recorded | also output ports, same names | -| `TailLedger` | one | per path | -| `Studies`, `ExecuteFrom` | prefix by index | cache per node, stage 8 | -| `PartialCoherence` | one run per mode | unaffected: the graph runs per mode | -| `Resonator`, `Oscillator` | nested sequences | stay, and give cross-checks for graph loops | - -## 6. The path ledger: phase and time - -`PathLedger` travels with each packet: - -```text -ArrivalS group delay of the carrier since the source (today's `arrival`) -PhaseRad carrier phase since the source (new) -SourceId which source this light came from -PhaseKnown false once an element could not report its phase -Dispersion, CumulativeLengthM, BIntegralRad today's locals -``` - -* `PhaseTerm` adds φ₀ and τ₀ to the ledger when it removes them from the frame. `DispersionPoint` gains a phase, so `own` still works. No field changes, so **all sequential results stay identical**. -* Active elements report phase through `ctx.Own`. One that does not sets `PhaseKnown = false`; a meeting downstream then warns that the fringe position is arbitrary, while the visibility remains right. -* Double precision is enough: 10 m is about 6×10⁷ rad, known to about 10⁻⁸ rad. -* When the carrier changes, the ledger follows: SHG gives φ₂ω = 2 φω plus the crystal's phase. An f-2f interferometer tests this. -* The sign follows `docs/concepts.md`, E = Re{A e^(−iω₀t)}. A wrong sign only moves fringes, and nothing else would catch that, so a test pins it. -* **Audit needed in stage 2.** The ray bundle and `DispersiveSurface` drop their piston. Spatial propagation uses k_z directly, and I have not confirmed how it treats the carrier term or the tracked beam's Gouy phase. Each must end up counted exactly once. - -### Meeting at a port - -With arm 2 arriving Δτ later, with phase difference Δφ, on a common grid in the frame of arm 1: - -```text -A₂(ω) -> A₂(ω) exp(i Δφ) exp(i (ω − ω₀) Δτ) (sign fixed by the convention test) -``` - -* **Grids.** Same grid: direct. Related: `GridResampler.Resample`. Different carriers must share a grid wide enough for both. -* **Polarisation.** x and y add separately, with x = p and y = s at a reflection. Signs of reflection belong to each element's definition. -* **Coherence.** Same `SourceId`, or `coherentWith`: fields add. Otherwise intensities add. - -### Packets - -Loops make this necessary at once: a cavity's output is a pulse train, and 10 ns of round trip cannot fit in a 10 ps window. So a beam on an edge is a **list of packets**, each a `PulseField` with its own ledger. A chain always has one. - -* Packets closer than the window merge coherently, by the sum above. Others stay separate. A single-frequency (`cw`) source always merges, so a cw loop gives the Airy result directly. -* Linear elements act on each packet, which is exact by superposition. Nonlinear elements do too, which is exact only if packets do not overlap in that element, and the compiler knows from the ledgers whether they do. -* Cost scales with packets, so `maxPackets` limits it; a packet below `tolerance` is dropped and booked as "truncated". -* Instruments: `energyMeter` and budgets sum all packets. Others default to the strongest, with a note, or take `"packet": n`. A `scope`-like view of the train comes later. - -Packets also solve pulse stacking, which the first draft had put off. - -### Beams meeting in space - -| model | what happens | -|---|---| -| 1-D with tracked Gaussians | fields add, scaled by the amplitude overlap of the two beams (q, position, angle), from their `BeamState`s. The rest is booked as "mode mismatch". The output follows the first arm | -| carried profiles | resampled to a common grid, added per frequency: tilt fringes appear by themselves | -| one profile, one not | error | - -Check: for two equal unit beams with visibility V, the two outputs of a lossless 50:50 splitter hold `1 + V cos Δφ` and `1 − V cos Δφ`. Beams crossing at an angle in a crystal do not meet at a splitter: they enter separate ports of the crystal. - -## 7. Loops - -A cycle is found as a strongly connected component of the graph. The compiler cuts its back edges, and the scheduler sweeps the rest repeatedly. One sweep is one pass: - -```text -pass 0: the external inputs run through the loop's acyclic part -pass n: the packets on the cut edges of pass n−1 run through it -output: packets leaving the loop on all passes, with their ledgers -``` - -| `kind` | stops when | output | use | -|---|---|---|---| -| `ringDown` | energy in the loop < `tolerance` × energy in, or `maxRoundTrips` | every packet leaving | etalons, Gires-Tournois, cavity ring-down, ring-down of a pulse | -| `steadyState` | the loop's field repeats within `tolerance`, or `maxRoundTrips` | the last pass | Fox-Li, a laser built from elements | - -* The first sweep is the sum of a geometric series for a linear loop, so it can be tested exactly. -* Gain with `stateId` keeps its state across passes, in arrival order. That allows a regenerative amplifier written out as elements. -* Light still in the loop at the end is booked as "left in loop". The budget still closes. -* The tracked beam is iterated too; a stable loop's q converges to the eigenmode, which the existing `Resonator` already computes. -* The `loop` key goes on the element where light enters the loop. Several external inputs share the settings of the first in file order. -* Several back edges in one component are allowed, and packets multiply. `maxPackets` stops the run with a message rather than silently truncating. -* `resonator` and `oscillator` stay. They are simpler for what they do, and they check the graph loop: the same cavity must give the same eigenmode, Fox-Li mode and loss. Their keys do not change. -* A linear cavity is a ring unfolded: forward and return paths are separate entries, joined with `sameAs`. A loop's elements have one direction each. - -## 8. New elements - -| element | ports | model | check | -|---|---|---|---| -| `beamSplitter` | `a`, `b` → `c`, `d` | 2×2 unitary, `reflectivity` or `coating`, `substrate` | Stokes relations; energy at any phase | -| `leakMirror` | `in` → `out`, `leak` | a splitter with small T(λ) from a mirror coating, each side's GDD | T integrated over the spectrum | -| `polarizingSplitter` | `a`, `b` → `c`, `d` | the `thinFilmPolarizer` model with both ports kept | Malus; extinction | -| `dichroic` | `in` → `c`, `d` | spectral R(λ) | sum of outputs = input at each λ | -| `delay` | `in` → `out` | a path or `delayS` on a stage | Michelson fringes of period λ/2 | - -These are variants of one definition, so reflection signs live in one place: side 2 has r' = −r\* t / t\* and t' = t. All work in sequential systems through `output` and virtual inputs. - -**Michelson and Sagnac interferometers** pass one splitter twice, but the light does not circulate, so this is not a loop. It is unfolded as an optician draws it: `{ "id": "bsBack", "type": "beamSplitter", "sameAs": "bs", "side": 2 }`. The graph stays acyclic and the signs are right by construction. - -## 9. Stored outputs and references - -`build/example_check.py --against ` runs every example with an older engine and compares every number in the summaries, so refactors are checked against the `d6fb4e9` engine with no stored files. That guards against change, not against an error that was always there. pyNLO cannot run in the owner's Python 3 environment, so the propagation cross-check in `CLAUDE.md` and the figures in `PROJECT_STATUS.md` are not currently reproducible here. That needs resolving separately, by a Python 3 route or a reworded claim. - -## 10. Modelled instruments - -An instrument is an ideal measurement of a field; a modelled one is the apparatus. The difference is what a user wants to know: *how far can I trust my autocorrelator?* - -### First result, already possible - -A real crystal in today's engine (`sfg`, a virtual copy of the pulse through `aux`, `delayScan`) against the `autocorrelator` instrument with default settings. Gaussian input, so ideal = √2 × FWHM. Run on 2026-10-30; a sketch, not yet a committed example. - -| pulse | BBO | crystal (fs) | instrument (fs) | ideal (fs) | crystal vs ideal | -|---|---|---:|---:|---:|---:| -| 100 fs | 20 µm | 141.48 | 141.52 | 141.42 | +0.04 % | -| 30 fs | 20 µm | 42.44 | 42.45 | 42.43 | +0.03 % | -| 30 fs | 1 mm | 42.66 | 42.45 | 42.43 | +0.5 % | -| 15 fs | 1 mm | 22.45 | 21.23 | 21.21 | +5.8 % | - -The two agree for thin crystals, and a thick crystal measures a 15 fs pulse 6 % long. The expected cause is phase-matching bandwidth and group-velocity mismatch, which the example should confirm. The instrument's `shgAcceptanceFwhmM` was off here; comparing it with the crystal deserves a test. - -### Plan - -Each is a `.ufs` in an example folder, with `report.py` comparing it with the instrument on the same pulse. - -| instrument | modelled as | what the model shows | -|---|---|---| -| `autocorrelator` | splitter, delay, noncollinear thin crystal, energy detector | crystal bandwidth, splitter GDD, crossing angle | -| `iac`, `frac` | Michelson, SHG, detector | the 8:1 ratio; dispersion imbalance; compensator plate | -| `crossCorrelator` | gate arm with its own line, SHG or THG | gate quality, dynamic range | -| Fourier-transform spectrometer | Michelson on a detector | Wiener-Khinchin: an analytic check against `spectrometer` | -| `frog` | the autocorrelator with a spectrometer | phase-matching filter on the trace; retrieval on a modelled trace | -| `dscan` | wedges, SHG, spectrometer (no branch needed) | SHG bandwidth | -| `contrast` | third-order cross-correlator | post-pulses making artefact pre-pulses | -| f-2f | SHG, dichroic, delay, spectrometer | CEP fringes: the end-to-end test of the ledger | - -Scanning is a study on `element:stage/lengthM`. One new feature is needed: a study that records a detector's *array* per point, for 2-D traces. - -## 11. Stages - -Each stage can merge alone and leaves the engine working. - -| stage | work | done when | -|---|---|---| -| **0. Prerequisites** | Commit this plan; `mkdocs --strict`; physics review of the ray bundle and T2; a baseline for `build/example_check.py --against` (the `d6fb4e9` engine); architecture docs and file headers ([section 14](#14-architecture-docs-and-file-headers)); ship the crystal autocorrelator example; first CI run when the owner pushes | review findings handled; docs build | -| **1. Ports and segments** | Port declarations for all elements; multi-port interface; `inputs` with `aux` as its spelling; segment runner; `PathLedger`-shaped state. `beamSplitter` works sequentially with `output` | reference outputs **bit-identical**; timing on three examples within 1 %; a splitter's ports sum to the input | -| **2. Ledger** | φ₀ through `DispersionPoint`; phase audit; report phase in `paths` | fields unchanged; phase of glass = n ω₀ L / c; fringe-direction test pins the sign | -| **3. Graph compiler** | Version 3, `topology`, `next` in both forms, port numbers, virtual ports with alerts, `SystemGraph`, all checks of [section 3](#3-compile-and-verify) including loops; `validate --graph`; Python `Sequential`, `Branched` | a test per check; a chain compiles to one segment; canonical form stable | -| **4. Acyclic execution** | Scheduler, packets, meeting at a port, window pre-pass, global budget, `delay`, `source` | Mach-Zehnder cos² fringes; energy at any phase; budget closes on every graph | -| **5. Loops** | Cut edges, sweeps, `ringDown`, `steadyState`, booking | ring and Fabry-Perot **Airy function**; Gires-Tournois group delay; graph Fox-Li = `resonator` | -| **6. Crystal ports, instruments** | Branch-fed crystal ports, by-product ports, `dichroic`, array studies, modelled AC, IAC, FTS, FROG, f-2f | branch-fed = `aux.pulse` to numerical precision; IAC 8:1; FTS = `spectrometer` | -| **7. Space** | Per-edge tracker, Gaussian overlap, profile meeting, `sameAs`, `polarizingSplitter`, `leakMirror`, tail ledgers | analytic Gaussian overlap; tilt fringe spacing λ/θ; visibility against overlap | -| **8. Node cache** | Upstream-hash cache for studies | same results as without; speed-up measured | - -Stages 1 and 2 change the most existing code and show the least, and they need the most care. Stages 3 to 5 are the core of the feature. Each stage updates the roadmap, history, `PROJECT_STATUS.md`, `docs/testing.md` and the reference pages. New pages: `docs/reference/branches.md`, and a tutorial on the Mach-Zehnder. - -## 12. Risks - -| risk | answer | -|---|---| -| Fringes that look right but are shifted | stage 2 tests; f-2f; `PhaseKnown` | -| A refactor changes sequential numbers | bit-identical outputs in stage 1, before anything is added | -| Packets multiply in nonlinear loops | `maxPackets`; merge in window; validation warns before running | -| Windows explode with long arms | packets; `validate` shows each arm's delay | -| Mis-wired systems | compile errors naming ports; `validate --graph`; a port takes one source | -| A model has perfect stability | say so in the docs: a modelled phase has no vibration or drift | -| No independent propagation check | [section 9](#9-stored-outputs-and-references) | - -## 13. Decisions - -Taken from the owner's review: `Sequential` and `Branched` forms, explicit `next` naming an input port, an internal graph, a compile and verify step, ports on all elements, alerts and virtual ports, loops in the first design. - -Recommended, and assumed unless the owner objects: - -| question | choice | -|---|---| -| Key names | `topology`, `next`, `inputs`, `output`, `loop` | -| Name | "branched system": in OpticStudio "non-sequential" means rays find their own order, which this does not do | -| Port numbers | start at 1, in declared order; names also accepted | -| Splitter ports | `a`, `b`, `c`, `d`, with `transmitted`, `reflected` as aliases | -| Michelson | unfolded with `sameAs`, not a loop | -| `resonator`, `oscillator` | stay, and check graph loops | -| First demonstrations | Mach-Zehnder, then Airy function, then modelled autocorrelator | - -## 14. Architecture docs and file headers - -The engine is about to gain its largest structural change, so its structure gets written down. - -* **`docs/architecture/`**: what the main folders, files and classes do, and the key processes (compile, execute, grids, studies, the graph). -* **File headers.** Every new or substantially changed source file starts with a short summary: what it does, its main classes, and their key signatures. This is a rule in `AGENTS.md`. -* Both are kept current with each stage. - -## 15. What changed in building it - -* **No `source` element.** A virtual beam on any input port is a source, so a separate element was not needed. -* **No `dump` element.** Unused outputs are virtual outputs. -* **No segments as objects.** The scheduler works element by element through `RunElement`, with the sequential loop kept as it was. -* **No cut edges.** Packets sent back wait in their target's queue for the next pass. -* **Pairing at a splitter was not needed.** It runs once per packet, which is exact for a linear element; interference happens when packets merge. -* **No window pre-pass.** Packets that do not fit a window stay separate. -* A virtual beam at a Sequential splitter must be coherent, since only one pulse continues. -* `leakMirror` has ports `in`, `back`, `out`, `leak`, and the splitter family is one class with coefficients and port names per type. -* The cache's nominal run stores and the others only read, so nothing depending on a scanned parameter accumulates. -* Tilts and offsets live on `delay`: arms need a way to be misaligned, and no other element owned that. -* Tails travel in the path ledger, so a branch's copy is automatic. -* A `steadyState` loop settles when both the field's shape and its energy repeat. Shape alone is not enough: a passive loop keeps its shape from the first pass while it decays. Unitarity means a lossless linear network cannot hold light, so a steady state needs gain. -* `graph.energyOutJ` counts every packet at `end`; the field in `RunResult.Output` is the strongest. A crystal's fed ports interact with one main packet, and other packets are booked, so fed energy is not used twice. -* Node cache speed-up, 81-point delay scan: 58.2 s to 9.6 s with 0.3 m of nonlinear glass upstream; 0.65 s to 0.60 s with 5 mm, where the crystal dominates. Rows are identical. The scanned parameter must be an `element:` override, which only that element's key contains. -* Not built: beams crossing at an angle in a crystal; a driven loop with the source fed every round trip. diff --git a/docs/design/history.md b/docs/design/history.md index 00362ad..e938888 100644 --- a/docs/design/history.md +++ b/docs/design/history.md @@ -114,8 +114,6 @@ Done in this order, each stage building on the one before: review of tests with ## Stage 5 -- branched systems (from 2026-10-30) -Plan: [branching.md](branching.md). - - [x] Ports on all element types, `PathLedger`, `SystemGraph` with verification, Sequential and Branched forms - [x] Interference in 1-D, packets, loops, crystal ports, `beamSplitter`, `delay`, virtual inputs - [x] An independent physics review of the T1 and T2 work, with its findings fixed @@ -129,10 +127,26 @@ Every spatial model and the branched interferometers were compared with LightPip - [x] Tests: `test_lightpipes.py`, `test_pynlo_branched.py`, `ReviewRegressionTests` - [x] Examples 32 to 34 moved to the Python API; 35 to 38 added; tutorial files moved to `docs/tutorials/files/` -## After 2.0 (plugins and breadth) -- [ ] Live-data plugin (streamed recordings, a digital twin that re-fits as data arrive) -- [ ] ML / PINN estimator and surrogate plugins -- [ ] Wheels for Windows/macOS/Linux on PyPI, NuGet package for the core -- [ ] OpticStudio User Extension (menu-integrated) in addition to the Interactive Extension - -Ideas and priorities are tracked in issues; contributions welcome (CONTRIBUTING.md). +## Stage 7 -- adaptive optics, temporal shaping and the digital twin (2026-11-09 to 10; five sessions S1-S5, merged to `main`) + +Owner decisions: transverse shaping first, temporal shaping too; "Zernike" means modes; the fibre array is a coherent tiled-aperture array; every device is parametric (no vendor models); +up to a full 1920 x 1080 SLM; the twin is its own plugin on top of fitting; CI on Linux (x64, arm64) and Windows, macOS by hand; Apache-2.0. + +- [x] **S1 foundation:** Python package split by what things are (shims keep every 2.2.0 name), `ultrafast.plotting` with one theme and `animate`, Zernike decomposition, examples regrouped into folders with `make_all.py` / `make_figures.py`, gallery generator +- [x] **S2 core:** `modulator` element (cells, fill factor, gaps, quantisation, cross-talk, chromatic rules), in-memory maps, `transverse.frequencyBlock`, `outputs.keepProfiles`, coherent tile source, slope maps from `wavefrontSensor`; scale spikes (a 1920 x 1080 panel runs; the memory peak is the .NET GC, 2.7-3.4 GB) +- [x] **S3 `ao` plugin:** `deformableMirror`, `slm`, `spectralShaper`, `aopdf`; `ultrafast_ao` (DM bench, tiled array, combining metrics); holography, control and sensorless algorithms; a physics review (15 findings, 9 fixed) +- [x] **S4 fitting and twin:** fitting across conditions (held-out conditions, `dependence` kinds up to network, surfaces), the `twin` plugin (project, validation, planner, what-if, drift tracker, goals, benchmark) +- [x] **S5 examples and docs:** 7 AdaptiveOptics, 2 DigitalTwin, 3 Fibres examples; twin docs + +## Stage 8 -- closing gaps toward the first release (2026-11-11; sessions A-F on `dev/2026-11`) + +- [x] **WP0 (A):** `UF_WITH_AO` in the native library, grid-planner hook for shaper side pulses (widens the window or warns), unknown-element message names `ao`, tests for Littrow dispersion, Bolduc and double phase through an SLM, tiled-array lobe fractions +- [x] **WP1 (B) thermal lenses in beam tracking:** distributed (quadratic-index) lens in the ABCD tracker, applied per pass in multipass and per round trip in regenerative amplifiers, effective focal length reported +- [x] **WP4 (B) plasma in envelope mode:** frequency-resolved Drude operator (`ionization.frequencyResolved`, default true), `electronDensityM3` for a prepared plasma; envelope against carrier-resolved within 1.2 % (blue shift) for MPI. The frequency-resolved shift is up to 5 % below the single-carrier formula for a 30 fs pulse at 1030 nm (the component check states both) +- [x] **WP2 (D) gain:** `aseModel` (steady-state forward / backward ASE per transverse cell and frequency, depletes the inversion between pulses; checked against n_sp h nu dnu (G-1), Frantz-Nodvik, saturated-amplifier law, cw photon balance); four-lobed stress-birefringence Jones map on cartesian grids (beam average equals the lumped value to 1e-6) +- [x] **WP3 (C) material catalogue:** all material, gas, Raman, gain and chi(2) data in embedded JSON with source, status and uncertainty; runs list every `approximate` / `unverified` value used (`summary.json` `unverifiedInputs`); `physics.strictMaterials`; TGG index refitted to the Schlarb-Sugg table (old fit up to 0.0106 off); 43 of 183 quantities `verified`; the YVO4 vendor sets stay (5e-4 / 9e-4 from Shi et al. 2001, recorded) +- [x] **WP5 (A) decisions:** no compile cache (a 128 x 128 trial costs 10 ms including compile), no full streaming executor (revisit for 4096 x 4096), tiles checked against a Branched balanced tree in a test; triggers in `docs/reference/speed.md` +- [x] **WP6/WP7 (A, E) findings:** `ny = 1` grids; cw line built at the requested carrier (cw against pulse agree to 1e-9); SPGD gain normalised by the running maximum (default reaches >= 0.9 over metric scales 1e-3 to 1e3; Spall sequences not built); Raman soliton shift rate explained (Gordon's first-moment approximation; engine within 2 % of the exact-gain rate and 0.3 % of pyNLO); SLM against Fourier explained (panel half a pixel off the grid; new warning); tiled-array reference explained (under-sampled lens, lock now at f = 1 m) +- [x] **WP8 (F) release system:** components and manifest, `install.py` (release and git sources, install, add, upgrade, uninstall), packager `--components`, ZOS-API gate, `release.yml`, 23 tests (see [releasing](../releasing.md)) +- [x] **Polish:** README picture grid and contact badge, logo palette and banner on every docs page, CI timeouts and apt retries, plasma component check split into carrier and frequency-resolved rows +- [x] **Follow-up (PR #1):** avalanche / inverse-bremsstrahlung double count fixed; catalogue defaults for user gain media and solids without data; `strictMaterialsAllow`; partial-override notes; Keldysh far-wing NaN. Spall gain sequences judged unnecessary (the running-maximum normalisation covers the range 1e-3 to 1e3) diff --git a/docs/design/next-steps.md b/docs/design/next-steps.md new file mode 100644 index 0000000..08b0691 --- /dev/null +++ b/docs/design/next-steps.md @@ -0,0 +1,56 @@ +# Next steps + +Open items after the November 2026 iteration (what was built: [history](history.md)). Longer-term design that is not scheduled: [backlog](backlog.md). +Rules of `CLAUDE.md` apply: a physics change needs a published or analytic reference in a test; a disagreement is a finding; tags and releases only when the owner asks. + +## 1. WP9: documentation overhaul (large; in waves) + +Every example README and landing page follows one structure instead of "the story": + +1. **Question** (one sentence: what is tested, why it matters). +2. **Setup:** the engine's schematic and a parameter table (SI units in the keys); the exact command. +3. **What is checked:** reference (analytic, published or a second code) and tolerance, stated before the result. +4. **Results:** hero figure, then a heat map or animation (every example gets one), the table of numbers, the quoted output. +5. **Extend it:** three or four concrete changes (keys to turn, what to expect, which example to look at next) with a Python or `.ufs` fragment. +6. **Limits and provenance:** what the model leaves out, open findings, unverified inputs (from the catalogue). + +Mechanics: `docs/examples/_template.md`; `build/example_lint.py` (required sections, hero, animation or heat map, quoted-output block, `# optional:` on figure scripts, no wall time in quoted output) run in CI beside +`example_check.py`; a one-file `example.json` per example (title, group, question, references, figures) feeding the gallery and group indexes. +Waves: (1) the 12 examples written in S5; (2) `Wavefronts`, `LaserSystems` (several results per chain: wavefront, contrast, FROG trace, beam maps), `Nonlinear`; (3) `Diffraction`, `Branched` +(with the cavity figure), `Diagnostics`, `Components`, `GettingStarted`, `DigitalTwin/transforms`; (4) landing pages (`docs/index`, `examples/README`, `concepts`), the plugin pages' "what is checked" sections. +Reference pages stay reference pages. Missing AO / twin examples (component gallery, complex-field shaping, spline against network) are written directly in the new structure. + +**Cavity figure** (part of WP9): `Branched/cavity-and-coherence` has no figure. Draw the Fox-Li iteration (mode converging round trip by round trip, radius against the ABCD eigenmode) and the Gaussian-Schell coherent modes and width against `w(z)`. + +## 2. Roadmap: needs the owner's input or a larger piece of work + +| item | why not now | +|---|---| +| **Verify approximate / unverified material values against their papers**, starting with CS2 (n2, 90/10 electronic-orientational split, 1.6 / 0.2 ps constants) and BaF2 (n2, sigma = -1.2); rotational Raman dephasing `T2At1Bar` of N2, O2, CO2, H2; the other n2 values | needs the owner's PDFs; the CREOL scans could not be rendered. Then re-run `liu-2013`, `shimizu-1967`, `kaur-2024` | +| LBO (Kato 1994) and the Jundt / Gayer LiNbO3 e-ray forms | no database page to compare with; needs the papers | +| Yb:YAG / Nd:YAG Fuchtbauer-Ladenburg consistency: do the catalogue's cross-section fits integrate to the radiative rate? | a one-hour check; the ASE source term depends on sigma_e through these fits (all `approximate`) | +| Birth phase of tunnelling electrons: drift-energy loss assumes a uniform birth phase; in carrier-resolved mode with PPT / ADK electrons are born near the crests, so the model books less loss (comparison test uses MPI) | needs a reference calculation (U_p at birth phase, Corkum 1993) and a change in how the energy is booked; not quick | +| Ray-based ASE for bulk crystals and disks, parasitic lasing, fibre use (backlog C3 builds on `AseModel`) | separate package | +| Stress: slice-by-slice retardation, the common-mode radial / tangential lenses on the field, vector gain for a y input | needs Koechner's formulas read from the source (not written from memory) and a reference for each; a package of its own | +| MCP server (T6): `ultrafast twin serve` exits with 2 | design below | +| GPU parity of the modulator and tile source; `release.yml` has never run on GitHub (Linux / Windows NativeAOT jobs, wheel in a fresh venv; macOS NativeAOT is built on the owner's Mac); its smoke test installs `examples.Branched` but does not run it | needs a GPU / a tag, which only the owner creates | +| Live-data plugin, ML / PINN plugins, PyPI wheels and a NuGet package (PyPI publishing is forbidden by the owner), OpticStudio User Extension | after the first release | + +## 3. Small items that are ready to do + +* `asePedestalEnergyFraction` is reported but not fed into the next stage's `pulse.pedestal`. +* A lens under-sampling warning (phase step per cell above pi at the beam edge) would have caught the tiled-array case. +* Python has no typed accessor for `unverifiedInputs`. +* The examples still pass `gain=` to SPGD explicitly; they could use the new default. + +## 4. MCP server: the agreed design (not built) + +Hand-written JSON-RPC 2.0 over newline-delimited stdio; no `mcp` dependency. Check the current MCP specification for the `protocolVersion` string and message shapes before writing. +Refusals are results with `isError: true`; unknown methods give -32601; notifications get no reply. + +* **Eight tools:** `describe`, `validate`, `run`, `compare_to_measured`, `scan`, `fit`, `explain_warning`, `plot`; workspace-scoped, read-only with respect to lab hardware. +* **Worker:** engine work runs in a persistent subprocess with its own stdout. On overrun it is killed and respawned. A pre-flight estimate (runs x grid points) refuses up front. Memory: `DOTNET_GCHeapHardLimit` + plus caps on N and nx*ny*spectralSamples; not `RLIMIT_AS`. The worker starts with `ULTRAFAST_HOME` set to an empty private folder so user plugins are not loaded. +* **Refuse by default:** strings starting with `@`, every callback / python / expression / user kind (list from `Schema/BuiltIn*.cs`); `set` narrowed to roots `pulse/`, `element:`, `instrument:` and never + `type`, `kind`, `measured`, `file`; values are finite numbers or identifier-pattern strings; file-valued strings must resolve inside the workspace; names match `^[A-Za-z0-9_-]{1,40}$`; unknown arguments rejected. +* **Tests:** `tools/list` returns eight tools; refusals for an expression, an `@name`, `../x.csv`, an absolute path, a symlink out of the workspace, and a call over its time budget (the next call must still work). diff --git a/docs/design/notes/s1-foundation.md b/docs/design/notes/s1-foundation.md deleted file mode 100644 index 17885b4..0000000 --- a/docs/design/notes/s1-foundation.md +++ /dev/null @@ -1,122 +0,0 @@ -# S1 Foundation: handoff notes - -Branch `dev/2026-11-s1-foundation` (from `dev/2026-11`), written 2026-11-09. **Status: S1 is done and merged to `main` (2026-11-09, commit `a875aec` plus the follow-ups in section 10).** What is not, -and what the next sessions must know, is at the end. - -## 1. The advisor call (Opus 5.5) and what it changed - -One call, after reading and before any edit. Applied: a committed snapshot of the 2.2.0 public surface before the split -(`python/tests/data/public_surface_2_2_0.json`) drives the import-every-old-name test; no submodule is named like a figure function -(`plotting.overview` is a function, the module is `run_figures`; `components.gain` is a function, the module is `amplifiers`); `ultrafast.diagram` -warns and no package module imports it; matplotlib and Pillow stay lazy (tested in a subprocess with `-W error::DeprecationWarning`); the generated API -docs were regenerated right after the move; the example regrouping checked ignored `results/` folders, relative `../` paths, the 08 -> 09 dependency and -`example_check.py` patterns before the single `git mv` pass. - -## 2. Old -> new names (Python) - -Nothing that was public in 2.2.0 disappeared. New names are additions; the only deprecated path is `ultrafast.diagram`. - -| old | new | how | -|---|---|---| -| `ultrafast.plotting` (one module of 1371 lines) | package `ultrafast.plotting`: `style`, `data`, `time_domain`, `spectral`, `run_figures`, `spatial`, `instrument_panels`, `diagrams`, `zernike`, `animate` | `__init__` re-exports every old public name and the private helpers the fitting plugin imports (`_figure`, `_has`, `_sanitize`, `_si`, `_Style`, `_theme`, `_cmap`, `fmt`, `load`, `_AXIS_LABELS`, `_fmtu`, `_named_axis`, `_value_unit`, `measurements`); `report`, `register_figure`, `plot`, `FIGURES` live in `__init__` because they look figures up by name | -| `ultrafast.diagram` | `ultrafast.plotting.diagrams` | `diagram.py` is a shim that re-exports everything and raises a `DeprecationWarning` on import | -| `ultrafast.plot` (the `ultrafast-plot` script) | unchanged | | -| `build.Sequential`, `Branched`, `link`, `loop`, `virtual_input`, `system` | `ultrafast.system.*` (+ `System`, `load`, `save`) | re-exports | -| `build.mirror`, `lens`, `grating`, `grating_pair`, `prism`, `plate`, `aperture`, `soft_aperture`, `mask`, `wavefront`, `chirped_mirror`, `dispersion`, `substrate`, `interface`, `nd_filter`, `filter_element`, `coating_ripple`, `probe`, `user` | `ultrafast.components.optics` (CapWords alias of each) | the same function object | -| `build.beam_splitter`, `polarizing_splitter`, `dichroic`, `leak_mirror`, `delay` | `components.splitters` | alias | -| `build.waveplate`, `polarizer`, `thin_film_polarizer`, `faraday_rotator` | `components.polarisation` | alias | -| `build.crystal` / `fiber` | `components.crystals` / `components.fibres` | alias | -| `build.gain`, `pump`, `multipass_amplifier`, `regenerative_amplifier`, `saturable_absorber`, `thermal_lens`, `resonator` | `components.amplifiers` | alias | -| `build.bulk`, `gap` | `components.media` | alias | -| `build.pulse`, `cw`, `measured_spectrum`, `measured_spectrum_csv`, `pedestal`, `gaussian_beam`, `constant_beam`, `sampled_beam`, `aux_beam`, `initial_profile`, `profile_ripple`, `profile_noise` | `ultrafast.sources` | alias | -| `build.instrument`, `axis`, `detector*`, `measured` | `ultrafast.instruments` + 18 explicit factories (`Spectrometer`, `Frog`, `DScan`, `Camera`, `WavefrontSensor`, ...) | real `def`s, so signatures and return types are visible to editors | -| `core.Result`; `plotting.SavedResult`, `load` | `ultrafast.results` | re-export; adds `fwhm`, `rms_width`, `strehl_marechal` | -| `core.retrieve`, `retrieve_multi`, `study` | `ultrafast.algorithms.retrieval`, `.optimisation` | re-export | -| (new) | `ultrafast.algorithms.zernike`, `ultrafast.plotting.zernike`, `ultrafast.plotting.animate` | | -| `build.run_spec`, `physics`, `grid`, `tail_grid`, `compute`, `sampling`, `save`, `override`, `environment`, `air`, `transverse`, `material`, `sellmeier`, `gas`, `coating_gdd`, `coating_quarter_wave`, `coherence`, `delay_uniform`, `delay_exponential`, `delay_table`, `delayed_scatter` | stay in `build` only | a decision, not an omission: they describe a run or a global setting, not a component, source or instrument. A test fails if a new public builder is neither grouped nor on this list | - -`ultrafast_fitting/figures.py` was **not** folded into the plotting package: the figure reads the fitting result format and is registered only when -the plugin loads, so core plotting must not import it. It keeps its file and imports the stable names from `ultrafast.plotting`. - -## 3. Examples: old number -> new path - -One `git mv` pass (ignored `results*/` folders moved with their folders). The table with old numbers is in `docs/examples/README.md`; `example_check.py` -accepts a name, a group or an old number (`08`). `LaserSystems/shg-lbo` sits with `nopcpa-bbo` (the roadmap had it under Nonlinear) because 09 reads 08's -results; `ultrafast-examples copy` brings it along. Group folders: GettingStarted (5), LaserSystems (10), Nonlinear (8), Diffraction (3), Wavefronts (2), -Branched (4), Diagnostics (7), Components (2), DigitalTwin (1: transforms), Interop (zemax-workflow, cpp), Computing (4, new), AdaptiveOptics (README only). -138 files had links rewritten by a script that resolves each relative path from the old location and re-expresses it from the new one. - -## 4. What was built - -* **Computing examples** (`docs/examples/Computing/`): 4f-correlator, optical-convolution, matrix-vector, parallelism-and-time, with a shared `_optics.py`. - The one rule: **f = N dx² / lambda** makes `lens + gap(f)` the DFT (the Fourier-plane pixels are the DFT bins), and a 4f system with a mask is the - convolution theorem. Numbers against numpy: lens as DFT 9e-3 relative l2 (random sparse complex vector), 4f identity 2e-3, blur 1e-4, edge filter 3.8e-3, - matched filter 3.8e-2 over the whole map with peak positions identical and heights within 0.5 %. -* **`docs/examples/make_all.py`** (`--group`, `--only`, `--no-animations`, `--jobs`, `--report`, size-budget check, table) and a `make_figures.py` in every example - (42) plus `make_all.py` per group. The generic `make_figures.py` runs the README commands and writes `figures/overview-*.png` (git-ignored) and, **only when the README shows no figure yet**, a committed `hero.png` next to it (19 READMEs now embed one); - it falls back to `ultrafast run run.json`, to the picture an example's own script wrote, and to the system schematic. Computing has purpose-made scripts that write a committed `hero.png` and `animation.gif`. READMEs link only committed files (a missing image breaks `mkdocs --strict`). -* **`build/package.py --examples-wheel`** -> `ultrafast_examples-2.2.0-py3-none-any.whl` (3.8 MB) with the `ultrafast-examples` command; extras `animate` and `examples` in `pyproject.toml`. -* **`build/gallery.py`** -> `docs/gallery.md` (in nav and `llms.txt`); `--hero NAME` copies an animation to `assets/hero.gif` for the README (S5 decides). - -## 5. Numbers - -* Animation budget: four GIFs, 39-329 kB (limit 2.5 MB); 23 committed hero PNGs, all within the 300 kB limit (the one file that was over it before S1, `Diffraction/iris-diffraction/diffraction-images.png`, was redrawn at 289 kB); 4.6 MB of committed media in total. -* `make_all.py --group Computing`: 4 examples in about 12 s on the owner's Mac. `parallelism-and-time` (N up to 1024): engine 0.02 ... 1.6 s, numpy FFT 0.00004 ... 0.02 s, optical model about 1e-6 s (4 f / c = 0.4 ... 6.8 ns plus an *assumed* 1 us exposure). -* Python tests: 173 pass, 1 skipped (was 127 at the last status update; the 38 new tests are `test_structure`, `test_zernike`, `test_computing`, `test_examples_package`). - -## 6. Commands - -```bash -python docs/examples/make_all.py [--group Computing] [--only 4f-correlator] [--no-animations] [--jobs 4] [--report r.json] -python build/gallery.py [--check] [--hero 4f-correlator] -python build/package.py --examples-wheel --out dist/ # then: pip install --find-links dist/ ultrafast-examples -python build/example_check.py shg-lbo Diffraction 26 # a name, a group or an old number -``` - -## 7. Changelog lines (for the merging session; `CHANGELOG.md` is a shared file) - -* Python: `ultrafast.plotting` is a package; new `ultrafast.system`, `components`, `sources`, `instruments`, `algorithms`, `results`; `ultrafast.diagram` moved to `ultrafast.plotting.diagrams` (the old name warns). -* Python: `algorithms.zernike` (Noll, NumPy), `plotting.zernike` decomposition figure (map, fit, residual, coefficient bars, rms and Maréchal Strehl; several wavefronts; animated), `plotting.animate` (GIF with Pillow, optional MP4, size budget). -* Examples regrouped into folders by topic (`docs/examples/README.md` has the old numbers); `make_all.py` and `make_figures.py` per example; new Computing examples (4f correlator, optical convolution, matrix-vector, parallelism and time); `docs/gallery.md`. -* Packaging: `build/package.py --examples-wheel` and the `ultrafast-examples` command; extras `animate`, `examples`. -* `.gitignore`: `!python/src/ultrafast/results/` (the `results/` rule had hidden the new Python package from git; CI caught it). - -`docs/testing.md` rows to add (shared file): Python structure (every 2.2.0 name imports, shim warns, matplotlib lazy, builder table complete); Zernike (orthonormal to 1e-12 by quadrature, -engine `wavefrontSensor` coefficients to 1e-6, Maréchal); GIF writer (frames, size, loop, held last frame); Computing (lens = DFT 2e-2, shift theorem 0.05 px, each example's own checks); -examples wheel (metadata, contents, `list` / `copy` / `run`). - -## 8. Not done, and for the next sessions - -* **Not run here:** `net48` (no Mono on the owner's Mac); `ultrafast-examples run` of a CLI-driven example needs the `ultrafast` program on the PATH (only Python-driven ones were exercised end to end). -* **Figures:** 23 examples have a committed hero (19 generic overviews + 4 purpose-made), 25 already embedded their own figure, and `Branched/cavity-and-coherence` has none (a cw / resonator run has no pulse to draw; its schematic is a single box). S5 decides which examples get a purpose-made figure and a committed animation, and the repository-size question of section 8 of the roadmap. -* **Mask phase is sampled on the grid.** A phase map with detail at every pixel (a matched filter) costs about 4e-2 relative l2 over the whole map; smooth filters cost 1e-4. S2 (`modulator`) should keep this in mind. -* **The lens is round.** A general matrix-vector multiplier needs an anamorphic layout (cylindrical lenses). Note for S2: a `lens` with separate x/y focal lengths. -* **Thin grids:** a "1-D" system uses `ny = 4` rows with `halfWidthYM = ny dx / 2` (so dy = dx). `ny = 1` fails with a `measured` profile (the input was not reproduced); not investigated further. -* **Grid coordinates:** the cartesian grid is `x_i = (i - N/2) dx` (it contains 0). A map written with half-pixel-centred axes is interpolated and blurred. -* `docs/examples/README.md` prose in the example READMEs still says "example 08", "example 26": the old numbers are in the index table; rewriting the prose was left alone. - -## 9. Verification of the finished S1 (2026-11-09) - -C# `--slow` on net10.0: 468 pass, 0 fail, 8 skipped (7 GPU, 1 opt-in HAPI); net48 not run (no Mono). Python with `ULTRAFAST_SLOW=1` and pyNLO: 173 pass, 1 skipped (no FP64 GPU). -Generated files unchanged by regeneration. `mkdocs build --strict`: 0 warnings. `make_all.py --group Computing`: 0 problems; `gallery.py --check` current; the examples wheel builds. -`example_check.py`: the first full run reported 9 problems. Eight were the test runner using a stale `bin/native` engine (no `m2Y`, `packetsAtEnd`, `externalAngleDeg`); with `ULTRAFAST_LIBRARY` set to -the hosted build they all reproduce their README numbers. **One was a real regression of the regrouping**: scripts that locate `python/src` with a fixed number of `..` or `parents[n]` (15 files) -and `compare_pynlo.py` ran only because `PYTHONPATH` was set; they now add the extra level (checked by evaluating each path). Lesson: after moving folders, grep for `"..", ".."` and `parents[`. -The test runner's example runs also rewrote two tracked PNGs (`f2f-cep/f2f.png`, `interferometers/interferometers.png`); they were restored from the index. -`google417a1b176c5dfb1e.html` (Search Console) was moved into `docs/` so it is published at the site root; the meta tag in `overrides/main.html` covers every page. - -## 10. Merge to `main` and follow-ups - -* `dev/2026-11` and `main` were fast-forwarded to `a875aec` (the branch point `3b875af` was an ancestor), at the owner's request, **before S5**: the roadmap's rule "merge to main after S5, `/verify` and green CI" was set aside for this one merge so the new examples, branding and the Search Console tag go live. The C# job of that run had not finished when the merge was made. -* The Windows Python job failed on `test_run_executes_the_readme_commands_in_a_copy`: `ultrafast-examples run` ran README lines through `bash -c`, which is not usable on a Windows runner. Fixed on `dev/2026-11`: plain `python ...` / `ultrafast ...` lines run directly (comments stripped), lines with shell syntax need a working `bash` and are skipped with a message when there is none. Not yet confirmed by a Windows run. -* CI warnings removed: Node 20 actions (`checkout` v7, `setup-python` v7, `setup-dotnet` v6, `upload-artifact` v7) and the TOML-table license in `pyproject.toml`. -* Lessons, for the next sessions: (1) a `.gitignore` rule such as `results/` can hide a new Python package: check `git status --ignored` after adding a folder; (2) after moving folders, grep for `".."`, `parents[` and relative paths in scripts, and run the examples **without** `PYTHONPATH` set (the editable install of `ultrafast-engine` hides path bugs) and with `ULTRAFAST_LIBRARY` pointing at the hosted build (a stale `bin/native` made 8 examples fail in one verification); (3) print errors as bounds, never as rounded last digits, when a README quotes them. - -## 11. What S2 should know first - -* S2 owns `src/UltrafastEngine.Core/**` (and the tests there). Nothing in `src/**` or `plugins/**` was touched in S1. -* The cartesian grid is `x_i = (i - N/2) dx`; a `mask` file or `measured` profile on half-pixel-centred axes is interpolated and blurred. A thin grid needs `halfWidthYM` for a y pitch equal to dx. -* The existing `mask` of kind `file` already takes amplitude and `phase_rad` maps; it reproduces smooth filters to 1e-4 and a per-pixel phase map (a matched filter) to about 4e-2 over the map. The `modulator` should say how it samples. -* A `lens` is round: no cylindrical or separate x / y focal length (S2 note for anamorphic optics and the matrix-vector multiplier). -* `ny = 1` with a `measured` profile did not reproduce the input; `ny = 4` works. -* The spike scripts can start from `docs/examples/Computing/_optics.py` (a 4f and a single-lens processor with a numpy twin); critical sampling f = N dx² / lambda is the rule that makes the engine's lens the DFT. diff --git a/docs/design/notes/s2-core.md b/docs/design/notes/s2-core.md deleted file mode 100644 index c5d27dc..0000000 --- a/docs/design/notes/s2-core.md +++ /dev/null @@ -1,64 +0,0 @@ -# S2 Core and spikes: handoff - -Branch `dev/2026-11-s2-core` (from `dev/2026-11`). Measurements and decisions: [spikes.md](spikes.md). This page: what was shipped, -what was not, and what the `ao` plugin (S3) may rely on. - -## Shipped - -| primitive | where | API as shipped | -|---|---|---| -| **C1 `modulator`** | `Optics/ModulatorElement.cs`, `Engine/CompilerModulator.cs`, `Schema/BuiltInElements.cs` | element `type: "modulator"`; keys in [reference/modulator.md](../../reference/modulator.md): cell grid (`cellsX/Y`, `pitchX/YM`, `offsetX/YM`, `rotationRad`), `phaseRad` / `opdM` / `amplitude`, `fillFactor`, `gapTransmission`, `outsideTransmission`, `zeroOrderLeak`, `crosstalkSigmaCells` or `crosstalkKernel`, `quantisationLevels`, `phaseWrapRad`, `chromatic` = `opd` (= `phaseAtDesign`) / `fixedPhase` / `index` (`deltaIndexCauchy` or `deltaIndex` + `deltaIndexWavelengthsM`); Python `ultrafast.build.modulator`, `ultrafast.components.Modulator` | -| **C2 in-memory maps** | same | `phaseRad` / `opdM` / `amplitude` as a nested array (numpy converted by the builder), a callback `"@name"` (`uf.run(..., fields={"name": f})`; C API `uf_run_set_field_callback`; `f(x, y, z)` at the cell centres) or an NPZ `file`. The three are tested identical to 1e-12; a callback is read at every compile, so **N loop trials = N file runs** (1e-12) | -| **C3 streaming and sparse sampling** | `Transverse/SpatialPropagation.cs` (`ExecuteLinear`), `Engine/Executor.cs` (`ProfilesInUse`), `Transverse/TransverseProfile.cs` (`TransverseOps.InterpolationError`) | `transverse.frequencyBlock` (blocks of stored frequencies in linear spatial elements; = all at once to 1e-12), `outputs.keepProfiles: "used"`; the sparse sampler is `transverse.spectralSamples`; record value `spectralSamplingError` (tested to bound the true focal-map error at K = 8, 16, 32) and warnings above 0.05 or a phase step over pi/2 | -| **C4 coherent tiles** | `Transverse/TileSource.cs`, `FromShape` | `initialProfile.kind = "tiles"`; Python `ultrafast.sources.TilesProfile` / `build.tiles_profile`. Delay sign: `delaysS > 0` = later (tested against a `delay` element) | -| **C5 fast path and zonal outputs** | `Instruments/CameraInstruments.cs`, `TransverseOps.SampleBins` | `spectralSamples: 1` stores the carrier (bug fixed: it was the low band edge); `wavefrontSensor` arrays `slope_x_rad`, `slope_y_rad` `[x][y]` next to `wavefront_waves`, `x_m`, `y_m` (tilt and defocus tested) | -| **C6 GPU parity** | `tests/.../GpuTests.cs` | `TilesAndModulator_IntoANonlinearPlate_MatchTheCpu` (1e-9); **skipped on this Mac** (OpenCL without FP64, no PoCL wheel); the body was checked CPU against CPU | -| cylindrical lens (S1 finding) | existing `lens` | `focalLengthXM`, `focalLengthYM` were already there; a test now covers the cylindrical case (`focalLengthYM: 1e30`) | - -## Not shipped, and why - -* **Whole-executor streaming** (instruments accumulating per frequency, so no full profile ever exists). Not needed: 1920 x 1080 at K = 16 - fits in 3.6 GB, and `keepProfiles: "used"` keeps the live set at about one profile. The process peak is set by the .NET GC (2.7-3.4 GB at - 1920 x 1080 x 16 for every block size), not by the propagator. -* **A compile cache (C2).** A 128 x 128 trial costs 10 ms including the compile; the file map was the cost (46 ms), and callbacks remove it. -* **GPU parity on a device**: the test exists and skips; run it on a CUDA / FP64 OpenCL machine or Linux with PoCL. -* **S5-tiles as a Branched system** was not measured; a Branched system cannot give an array's far field (spikes section 5). The tile source - is tested against the array theorem instead. -* **Roadmap test "cw equals the pulse at the carrier (1e-9)"**: the stored frequency of a pulse is within 1e-7 of the carrier (one grid bin of a - 1 ns grid). A cw source's single bin is snapped to its grid lattice, 1.2e-6 off the requested carrier (printed in the run notes), so cw and - pulse focal patterns agree to 3e-6, not 1e-9. Moving the cw line would change every cw result for a 1e-6 effect; not done. - -## What the `ao` plugin may rely on - -* A map for a device is computed by the plugin (influence functions, voltage curves) and given as `opdM` (surface or path) - or `phaseRad` + `phaseWrapRad` + `quantisationLevels`; `chromatic: "opd"` makes a wrapped map a grating; the core does not wrap unless asked. -* A liquid-crystal SLM with a dispersive birefringence: `chromatic: "index"` with the plugin's Delta-n as `deltaIndexCauchy` [A, B_m2, C_m4] - or a `deltaIndex` table; the phase map is the one written at `designWavelengthM`. -* For a deformable mirror use `opdM` = 2 cos(aoi) x surface height (the surface-to-wavefront factor is the plugin's). -* `fillFactor`, `crosstalkSigmaCells`, `zeroOrderLeak` and the gap / outside transmissions exist; a hex actuator layout is not a cell grid: map it to - a square cell grid (the plugin resamples) or use several modulators. -* Use at least 2 (better 4) grid cells per device cell, with the array edges on grid-cell edges (`offsetXM` half a grid step) to avoid the partial-cell - amplitude loss (booked as `clip`). -* Tile delays need `spectralSamples` large enough: spacing of stored frequencies x delay under ~0.5 rad. -* A control loop calls the engine with a callback map; the spikes give 10 ms per 128^2 trial. - -## Numbers (also in spikes.md) - -1920 x 1080: 3.3 s / 1.05 GB (cw, Python); on a 2048 x 1280 grid (C#) 1.9 s, 260 MB held after the run with `keepProfiles: "used"` against 491 MB with `all`; 15.7 s / 1.74 GB (K = 8), -32 s / 3.6 GB (K = 16). Live set after a five-element 1024 x 1024 x 16 run: 1587 MB (`all`) -> 336 MB (`used`). First-order efficiency of -an N = 4, 8, 16 blaze within 0.6 % of the sampled staircase and 2 % of the continuum formula. Tile combining within 2 %. - -## Changelog lines - -Written into `CHANGELOG.md` (Unreleased: Added, Fixed). `docs/testing.md` rows for `ModulatorTests`, `TileSourceTests`, `SpatialStreamingTests`. -`PROJECT_STATUS.md` counts, `docs/capabilities.md`, `ROADMAP.md` and `docs/roadmap.md` updated by this session at the owner's request (it merges -S2 into `dev/2026-11`). - -## Known sharp edges - -* A grid cell split between two device cells with different phases transmits less than 1 (one complex number per cell). Not a bug: the missing - power is spatial frequency the grid cannot carry. See the modulator page. -* The modulator multiplies by `exp(i phi omega / omega_design)` exactly; it does not protect against an unresolved phase: raise `spectralSamples` when `spectralSamplingError` warns. -* A callback map is evaluated once per run (at compile time), not per frequency. -* A `delay` element moves the arrival time (the frame), not the field; a tile delay is relative to the frame and moves the field. -* `outsideTransmission` defaults to 0: light beyond the array is blocked (a small DM in a large grid clips the beam). diff --git a/docs/design/notes/s3-ao.md b/docs/design/notes/s3-ao.md deleted file mode 100644 index afa27ad..0000000 --- a/docs/design/notes/s3-ao.md +++ /dev/null @@ -1,113 +0,0 @@ -# S3 AO plugin: handoff - -Branch `dev/2026-11-s3-ao` (from `dev/2026-11` at 5469481). Page for users: [docs/plugins/ao.md](../../plugins/ao.md) (parameter tables, the -physics of each device, what each test checks). This page: what shipped, what did not, numbers, and what S5 may rely on. - -## Shipped - -| roadmap item | where | state | -|---|---|---| -| AO0 scaffold | `plugins/ao/` (`plugin.json`, `src/*.csproj`, `python/ultrafast_ao`), sln, test project reference, `build/CopyPlugins.targets`, `build/package.py` (`ao`, aliases `adaptive-optics`, `slm`, `dm`), `python/tests/test_packaging.py` | done; the default plugin list is now fitting, zemax, gpu, ao, hitran | -| AO1 `deformableMirror` | `plugins/ao/src/Devices.cs` | done: square / hex / ring / explicit layouts, Gaussian influence with stated coupling, stroke, drive bits, `2 cos(aoi)`, clear aperture; **hysteresis, creep, print-through, segmented and bimorph kinds not built** | -| AO2 `slm` | `Devices.cs` | done: pixels, pitch, fill factor, grey levels, linear / tanh / table curves, phase target to the nearest level, liquid-crystal `dn(lambda)` (Cauchy), cross-talk, zero-order leak; 1920 x 1080 as callback or NPZ; **the Delta-n preset is generic and unverified** | -| AO3 `tiledArray` | `ultrafast_ao.tiled_array`, `combining_metrics` | done as a **builder of the core tile source** (the advisor's correction: a source is not an element); no nested per-channel systems | -| AO4 sensing | `ultrafast.algorithms.control`: `shack_hartmann` (lenslet FFT + windowed centroid, noise), `modal_fit`, `southwell_reconstructor` | done, NumPy; the bench uses the engine's `wavefrontSensor` as the ideal zonal sensor (the engine has no complex-field output to feed the lenslet model) | -| AO5 calibrate / correct / loop | `control.interaction_matrix`, `control_matrix` (Tikhonov, TSVD), `correct`, `integrator_loop`; `ultrafast_ao.DmBench.calibrate / correct / loop` | done; **no `ultrafast ao ...` CLI commands** (they are Python) | -| AO6 holograms | `ultrafast.algorithms.holography`: GS (quantisation in the loop), WGS, MRAF, double-phase, Bolduc, `grey_levels` | done | -| AO7 sensorless | `sensorless`: SPGD, hill climb, modal search, phase-stepping transmission matrix | done; phase locking = SPGD on the tile pistons (tested through the engine) | -| TS1 `spectralShaper` | `plugins/ao/src/TemporalShapers.cs` | done (1-D transfer function; spot overlap by erf) | -| TS2 `aopdf` | same | done (polynomial or table phase, window, efficiency, replica); closed-loop shaping tested with the engine's peak power as the metric (GDD compensation); **FROG / d-scan as the metric is not wired** (the metric is any function; the engine's `frog` / `dscan` instruments can be called by the user) | -| physics review | `physics-reviewer` pass | see the end of this page | - -## Numbers (development laptop, Apple silicon, CPU) - -* 1920 x 1080 `slm`, 256 grey levels from a callback, cw, grid 2048 x 1280, `keepProfiles: "used"`: **2.4 s** (S2 limit 13 s). -* `DmBench` (128 x 128 cells, f = 0.5 m): about 0.1 s per run; calibration of 100 actuators (200 pokes) 7 s; modal search over six modes (54 runs) about 5 s. -* One-shot zonal correction of {Z4: 0.3, Z5: -0.2, Z7: 0.15} waves rms: Strehl **0.31 -> 0.87** (10 x 10 actuators of 0.4 mm), modal (Zernike fit) **0.965**; the loop at gain 0.7 reduces the slope rms by 0.3 per step (1 - g). -* Strehl against theory: the engine's focal peak is 0.5-2.4 % above the exact on-axis value `||^2` for rotationally symmetric aberrations (the peak may leave the axis); Marechal `exp(-sigma^2)` is within 1 % at sigma = 0.25 rad and 5 % at 0.5 rad (defocus + astigmatism). -* SLM blazed efficiency: within 0.6 % of the sampled staircase for N = 4, 8, 16; at other wavelengths within 1.5 % of the `s(lambda)` law (900, 1030, 1200 nm). -* Weiner side pulses of a gapped shaper: energy ratio `[sin(pi f)/(pi f)]^2 exp(-2 (pi w0/pitch)^2)` within 1 %, position within 1 fs. -* Tile delay cost: 0.3 %, 0.04 %, 0.65 % from the Gaussian-overlap formula for rms delays of 10, 25, 40 fs (7 tiles, 30 fs). -* Transmission matrix: phases exact to 1e-9 rad; focusing enhancement over 3000 random outputs of N = 64: 50.40 against (pi/4)(N - 1) + 1 = 50.48 (standard error 0.07). -* GS: quantisation in the loop helps at 2 and 3 levels (6 of 6 seeds), **not** at 4 or 8 on the ring target. -* SPGD, 16 channels, 400 iterations: final lock 1.000 (minimum over 10 seeds) at gain 10, amplitude 0.2; gain 20 diverges (mean 0.67). - -## What S5 (examples) may call - -* `ultrafast_ao`: `DeformableMirror`, `Slm`, `SpectralShaper`, `Aopdf` (element dicts), `DmBench`, `tiled_array`, `combining_metrics`, `devices.*` twins. `DmBench(aberration={noll: waves}).strehl() / .calibrate() / .correct(D) / .loop(D) / .zernike_commands(coefs)`. -* `ultrafast.algorithms.holography / control / sensorless` (no plugin needed). `ultrafast.algorithms.zernike` for the aberration plots (S1). -* **Pitfalls for example authors** (all in `docs/plugins/ao.md`): the shaper needs `grid.windowS` to cover `2 pi / pixel bandwidth`; the engine's focal plane is sampled at the simulation pitch, not at `lambda f / (N dx)` (keep the pattern inside the window); give big SLM maps as a callback or file; a tile delay moves the envelope only; the tile source is normalised to the total power; the beam profile peak should be refined between pixels (`DmBench.peak_fluence` does). - -## Not done / for the owner or later sessions - -* **NativeAOT build does not compile the plugin in** (`UF_WITH_AO` in `src/UltrafastEngine.Native` is S2's area). The hosted engine (Python default, CLI) loads it. Request: add `UF_WITH_AO` as for `UF_WITH_HITRAN` if the native Python wheel should carry it. -* **Grid planner does not know a shaper's time extent** (`2 pi / pixel bandwidth` side pulses): the user sets `grid.windowS`. A hook in `GridPlanner` (core, S2's area) could read it from the term. -* **GPU parity** of the S2 primitives and the AO elements was never run on a device; the plugin adds nothing GPU-specific (maps are computed in C# and applied by the core modulator). -* **net48**: the solution builds for net48 (0 errors); the tests were run on net10.0 only (no Mono on this Mac). -* Edits outside `plugins/ao/**` that were needed: `UltrafastEngine.sln`, `build/CopyPlugins.targets`, `build/package.py`, `tests/UltrafastEngine.Tests/{UltrafastEngine.Tests.csproj,TestFramework.cs}` (reference and register the plugin), `SchemaTests.cs` (the shipped-origins list gained `"ao"`), `python/tests/test_packaging.py`, `python/src/ultrafast/algorithms/{__init__,holography,control,sensorless}.py`, `docs/plugins/ao.md`, `plugins/CLAUDE.md`, and the regenerated `schemas/*`, `docs/reference/options.md`, `python-api.md`, `validation-matrix.md`, `types.py`. -* **Shared files left for S5** (per the roadmap's ownership rule): `mkdocs.yml` and `llms.txt` (add `plugins/ao.md` to the nav; the page builds under `--strict` without being in the nav), `docs/plugins/index.md`, `docs/reference/plugins.md` (first-party list), `CHANGELOG.md`, `docs/testing.md`, `PROJECT_STATUS.md`, `docs/capabilities.md`, `ROADMAP.md`, `docs/roadmap.md`. - -## Changelog lines (for S5 to paste into `CHANGELOG.md`, Unreleased) - -Added: -* `ao` plugin: `deformableMirror`, `slm`, `spectralShaper` and `aopdf` elements (parametric, no vendor data); the first two compile into the core `modulator`. -* `ultrafast.algorithms.holography` (Gerchberg-Saxton with quantisation in the loop, weighted GS, MRAF, double-phase and Bolduc encoding, grey-level mapping), `.control` (Shack-Hartmann lenslet model, Zernike modal and Southwell reconstructors, interaction / control matrices, integrator loop) and `.sensorless` (SPGD, hill climbing, modal search, phase-stepping transmission matrix). -* `ultrafast_ao`: `DmBench` (simulated AO bench that drives the engine), `tiled_array` and `combining_metrics` for coherent tiled-aperture arrays, NumPy twins of the devices. - -## `docs/testing.md` rows - -| test file | tests | what | -|---|---|---| -| `AoDeviceTests.cs` | 20 (1 slow) | deformable mirror (tilt, 2 cos(aoi), coupling, stroke, hex), SLM (blaze, orders, liquid-crystal dispersion, nearest level, 1920 x 1080), erf, aopdf (delay, GDD, window, replica), spectral shaper (Weiner side pulses, delay) | -| `python/tests/test_ao_algorithms.py` | 19 | holography, sensing and control, sensorless, transmission matrix | -| `python/tests/test_ao_plugin.py` | 12 | Strehl and Marechal, device twins, fit residual, correction and loop, modal search, GS hologram through the slm element, tiled array, SPGD lock, GDD shaping | - -## Physics review - -One `physics-reviewer` pass (15 findings). **Fixed:** `spectralShaper.offsetM` cancelled out (now moves the array; test added); the frequency-to-position map is now exact in wavelength (was linear in omega); the AOPDF replica created energy (now split off the main pulse, total = efficiency; test added); the `deformableMirror` `commandsM` are documented as drive coefficients (a uniform command gives a ripple of about 1.7 x at coupling 0.15, not a flat piston); the nearest-grey-level search uses the circular distance for curves that span more than 2 pi (C# and Python; tests added); the Airy radius in `combining_metrics`; the unit hint (1e6 = 1 mm/nm); the `transmission_matrix` docstring (column 0 is a marker, cost 4(n-1)+2); the Weiner test now uses a 150 fs pulse and a 32 ps window because the spectral grid sampled the comb edges (5 samples per pixel gave 2.5 % errors: now in the docs). **Documented, not changed:** the field-overlap model of the spot (the Weiner test checks its implementation, not the choice); no anamorphic grating factor; a large programmed GDD or a shaper's side pulses can wrap around the time window without a warning (needs a planner hook in the core); `modal_fit` uses the gradient at the lenslet centre, not the subaperture mean. **Open (no test yet):** AOPDF GDD/TOD sign (red first for +GDD), grating-derived dispersion against a Littrow reference, SLM fill factor / gap / cross-talk through the plugin (covered by the core modulator tests), end-to-end Bolduc and double-phase through an SLM, drive-bit step size, tiled-array lobe fractions beyond `0 < f < 1`. - -## Brief items met differently - -* "A pulse through a wrapped phase ramp": three monochromatic runs (900, 1030, 1200 nm) of a blazed `slm` ramp against the `s(lambda)` law (the first-order efficiency of a pulse is the spectrum-weighted sum of these). -* The (pi/4)(N - 1) + 1 enhancement is tested on a synthetic random matrix in NumPy; no engine scatterer exists. -* FROG / d-scan is not wired as a loop metric; the closed-loop test uses the output peak power. Any function of a run can be the metric. -* Calibrate, correct and loop are Python (`DmBench`, `ultrafast.algorithms.control`), not `ultrafast ao ...` CLI commands. -* Core request: the unknown-element-type message could name the `ao` plugin for `deformableMirror`, `slm`, `spectralShaper`, `aopdf`, as it does for the other first-party types. - -## Verification (test-runner, before the review fixes) - -After the review fixes and the test hardening (final tree): net10.0 `--slow` **528 passed / 0 failed / 9 skipped** (GPU device and HITRAN reference-dir skips); `AoDeviceTests` 20 (including the slow 1920 x 1080 test); Python `test_ao_algorithms.py` 19 + `test_ao_plugin.py` 12 + `test_packaging.py`, all passed; `mypy` clean; `mkdocs build --strict` 0 warnings; the regenerated files are current. Earlier full run by `test-runner` (before the fixes): Python with pyNLO and slow 205 passed / 1 skipped (no GPU), `example_check.py` 0 problems, solution builds with 0 errors. Not run: net48 tests (no Mono), GPU, OpticStudio, NativeAOT inclusion of the plugin. - - -## Session close (2026-11-10): status, deferred and open items - -### State of the branch -* `dev/2026-11-s3-ao` is pushed and was **fast-forwarded into `dev/2026-11`** (bca9210) at the owner's request. **Nothing is merged to `main`; no tag or release.** -* CI on the push before the last commit: every job green except "Build (net10.0, net48) and C# tests", which failed on two wall-time budgets (23.1 s and 21.2 s against 13 s). Fixed in bca9210 by `SpatialStreamingTests.ScaleBudgetS()` (13 s locally, 45 s when `CI` is set). **That commit's CI run was not watched**: check it. net48 has therefore not been confirmed on this code locally (no Mono) and depends on that run. -* Final local numbers: net10.0 `--slow` 528 passed / 0 failed / 9 skipped before the budget fix (the two timing tests pass locally and with `CI=true` after it); Python ao tests 38 passed (`test_ao_algorithms.py` 19, `test_ao_plugin.py` 12, `test_packaging.py`); earlier full Python run 205 passed / 1 skipped; `mypy` clean; `mkdocs build --strict` 0 warnings; `example_check.py` 0 problems; regenerated files current. - -### Done (all of it) -* Plugin `ao`: `deformableMirror`, `slm`, `spectralShaper`, `aopdf` (C#); `ultrafast_ao` (builders, device twins, `DmBench`, `tiled_array`, `combining_metrics`); algorithms `holography`, `control`, `sensorless` in `ultrafast.algorithms`. -* Wiring outside `plugins/ao`: sln, test project reference and registration, `build/CopyPlugins.targets`, `build/package.py`, `test_packaging.py`, `SchemaTests` shipped origins, `PluginTests` (ao registered, folder loads, four elements declared by `ao`), the two scale-budget tests, regenerated schemas / options / types / API docs / validation matrix. -* Docs: `docs/plugins/ao.md`, `plugins/ao/README.md`, `plugins/CLAUDE.md`, this page, the roadmap Progress table, the memory file. -* Reviews: advisor call 1 (before the SLM model), advisor call 2 (before done: led to the replica test, drive-step test, AOPDF sign test, plugin-registration test, GS phase check, end-to-end modal fit, attribution trailers), one `physics-reviewer` pass (15 findings). -* Test hardening after review: Reference strings now say what is checked; measured spreads are quoted instead of remembered; the AOPDF GDD/TOD sign is checked against the core `dispersion` element (peak and FWHM within 1e-3, centroid within 0.1 fs, +6.2 fs from TOD); the fill-factor replica (1/9 at lambda f / pitch) is tested through the `slm` element. - -### Not done (by design or for lack of time) -* Shared files, left for S5: `CHANGELOG.md`, `docs/testing.md`, `PROJECT_STATUS.md`, `docs/capabilities.md`, `ROADMAP.md`, `docs/roadmap.md`, `mkdocs.yml`, `llms.txt`, `docs/plugins/index.md`, `docs/reference/plugins.md`, the README test-count badges. `docs/plugins/ao.md` is not in the nav yet (it builds under `--strict`). -* Not run: net48 tests locally; GPU (parity of the S2 primitives and the ao elements never ran on a device); OpticStudio; NativeAOT inclusion (needs `UF_WITH_AO` in `src/UltrafastEngine.Native`, core area). -* Not built: DM hysteresis, creep, print-through, segmented and bimorph kinds; per-channel nested systems in the tile array; FROG / d-scan as a closed-loop metric (the closed-loop test uses peak power; any function of a run can be the metric); `ultrafast ao ...` CLI commands (calibrate / correct / loop are Python); TS3 (the 4f line with spatial chirp); an engine-side Shack-Hartmann instrument (the physical lenslet model is NumPy and needs a complex field you build yourself; the bench uses the ideal `wavefrontSensor`); the plugin's `requires` key (not needed, the optimisers are Python). -* Untested: the grating-derived dispersion of `spectralShaper` against a Littrow reference; end-to-end Bolduc and double-phase through an SLM with a spatially varying field; the Zernike-gradient bias of `modal_fit` for Noll >= 7 (gradient at the lenslet centre, not the subaperture mean); lobe fractions of `combining_metrics` beyond `0 < f < 1`; SLM fill / gap / cross-talk through the plugin (the core modulator tests cover the core). -* Known modelling limits (documented in `docs/plugins/ao.md`): field-overlap spot model of the shaper (the Weiner test checks the implementation, not the choice); no anamorphic grating factor; constant `D` over the band; `commandsM` are drive coefficients (a uniform command gives about 1.7 x at coupling 0.15); a zero command with `driveBits` becomes a half-step piston. - -### Still to do (owner or later sessions) -1. **Watch the CI run of bca9210**; if "Build (net10.0, net48) and C# tests" is red for a reason other than timing, fix it before merging to `main`. -2. **S5** merges the shared files above, adds `docs/plugins/ao.md` to the nav, writes the AO examples (it may call everything under "What S5 may call"), and runs `/verify` before any merge to `main`. -3. **Core requests** (S2's area, the owner decides): `UF_WITH_AO` in the Native project; a grid-planner hook so a shaper's `2 pi / pixel bandwidth` side pulses and a large programmed GDD extend the time window (today nothing warns when the output wraps round the window); the unknown-element-type message should name `ao` for the four types. -4. **Run on a GPU / PoCL machine** the S2 parity test `GpuTests.TilesAndModulator_IntoANonlinearPlate_MatchTheCpu`, and net48 under Mono, before a release. -5. **Replace the generic SLM birefringence preset** with a published or measured Delta-n(lambda) when one is available; until then it is labelled unverified everywhere. -6. Optional follow-ups: the untested items above; FROG / d-scan as the shaping metric; a GS between tile pistons and the far field for fibre-bundle beam shaping (next section). - -### Question answered at the end of the session: beam shaping with a 2-D bundle of fibre outputs -**Partly supported.** The core tile source (`initialProfile` kind `tiles`, S2) models a grid of closely spaced coherent outputs that interfere: square, hex, ring or explicit positions, with per-tile piston, tilt, delay and amplitude, summed coherently (overlap included, total power normalised). A lens and a gap give the far field. `ultrafast_ao.tiled_array` builds it and `combining_metrics` scores it; the tests check the N-fold peak, the piston cost `|sum e^{i phi}|^2 / N^2`, the delay cost (Gaussian envelope overlap) and SPGD phase locking through the engine. -**Missing for a fibre bundle:** (a) nothing computes the pistons for a *target pattern* on the tile grid (the GS / WGS routines work on pixel grids; a GS between the tile pistons and the far field is a small addition); (b) every tile has one radius and a simple shape (Gaussian, tophat, truncated): no fibre mode-field / numerical-aperture profile, no per-tile radius; (c) no core-to-core coupling (multicore fibres and imaging bundles were out of scope in the plan); (d) per-channel dispersion or nonlinearity beyond a delay is not modelled. With few tiles the control is coarse (about one phase per tile): beam steering and far-field shaping, not detailed images. diff --git a/docs/design/notes/s4-twin.md b/docs/design/notes/s4-twin.md deleted file mode 100644 index 1c3a4a1..0000000 --- a/docs/design/notes/s4-twin.md +++ /dev/null @@ -1,155 +0,0 @@ -# S4 Fitting and twin: session notes - -Branch `dev/2026-11-s4-twin`, cut from `dev/2026-11` (which already contains S2 and S3). 2026-11-10. -Committed, pushed and merged into `dev/2026-11` at the owner's request while T6, the Python twin tests and the twin docs page are still open (section 4). -`/verify` result: C# 543 pass / 9 skipped; Python 207 pass / 1 skipped; mkdocs strict 0 warnings; regenerated files identical; all examples -pass (the `example_check` run only failed when `ULTRAFAST_LIBRARY` was not set to the hosted engine: a stale NativeAOT library lacked the S2/S3 keys). net48 tests not run (no Mono). -`serve` prints "not implemented" and exits 2. - -## 1. Summary - -| part | state | -|---|---| -| F1 multi-condition fit | **done**, tested | -| F2 parametric conditioning | **done**, tested | -| F3 smooth surfaces in (wavelength, condition) | **done**, tested | -| F4 network transforms | **done**, tested | -| F5 gradients note | **done** (a paragraph in `docs/plugins/fitting.md`; no measurement beyond run counts) | -| `twin` plugin: T1 project, T2 validation, T3 planner and design, T4 what-if, T5 drift tracking | **done in Python, exercised by hand; no automated Python tests yet** | -| T6 MCP server | **not started** (the security design is settled, section 5) | -| T7 goal file, checker, benchmark | **done** (`goals.py`); not yet in the automated tests | -| `ultrafast twin` command (C# dispatcher) | **written and builds; never run from the real CLI** | -| `docs/plugins/twin.md`, `plugins/twin/README.md`, `mkdocs.yml`, `llms.txt` | **not written** | -| `/regen` | done (schemas, options.md, types.py, validation matrix) | -| `/verify`, full test suite, Python suite, mkdocs strict | **not run** | - -## 2. Fitting plugin (F1-F5) - -Files: `Fitting.cs` (rewritten around conditions), `Dependence.cs` (new), `FitValues.cs` (`SetValue`), `FittingPlugin.cs` (spec keys), -`TransformOperators.cs` and `TransformElement.cs` (`nonlinearPhase`). Docs: new section "Fitting across conditions" in `docs/plugins/fitting.md`. - -**F1 design (advisor call 1).** -* `fit.conditions[]`: `name`, `variables`, `set` (path to number, string or object), `heldOut`, `weight`. -* A fit without conditions is one implicit condition; there is one code path. `TransformTests` (20) still pass unchanged. -* Each condition has its own frozen grid. Priors and smoothness penalties are added once after all conditions. -* Held-out conditions are never in the cost. They are simulated at the start, at the optimum and at the Jacobian points, so they get chi2, - curves and a prediction band from the same covariance. A held-out chi2 per point above 2 x max(1, training value) adds a warning. -* With conditions, `PerInstrument`, `Influence` and `Predictions` are keyed `condition/instrument`. `fit.json` gains `conditions[]`. -* `parameters[].perCondition` expands into one slot per training condition. A shared parameter that a condition also `set`s is an error. -* New output `fitted-conditions.json`: per condition the variables, `set`, grid, fitted values of dependent paths, and `dependences[]` - in a form `ultrafast_twin.DependenceModel` evaluates. `fitted-system.ufs` holds only the shared parameters. - -**F2 / F3 / F4 (`Dependence.cs`).** Kinds: `constant`, `proportional`, `linear`, `polynomial`, `power`, `chebyshev`, `network`. -Coefficients are ordinary fit parameters (origin `dependence` or `network`). `fit.parameters[].dependence` is one path; `fit.surfaces[]` -is many paths (`pathTemplate` + `count`) with `smoothnessPaths` and `smoothnessVariable`. - -**F4 constraints, written down.** A network is `hidden` tanh units on the scaled variable xi, one output per path, sharing the hidden layer. -* `output: "bounded"`: `lo + (hi-lo) sigmoid(raw)`, so strictly inside the range for any weights. -* `output: "positive"`: softplus. -* `monotone`: input and output weights are squared (sign flipped for `decreasing`), so the output cannot decrease in the variable, also - outside the training range. -* `weightDecay` (default 1e-4) is the only soft term. Weights are boxed to +-12 so CMA-ES can be used. -* It is not described as a PINN anywhere. - -**Added outside the brief:** the transform operator `nonlinearPhase` (`phi(t) = bRad I(t)/max I`, exactly invertible). Reason: no existing -operator changes a spectrum through a phase that depends on the energy, which the "phase linear in energy" test needs. - -**Tests.** `FittingConditionsTests.cs`: 15 tests, all pass (plus the 20 `TransformTests`). -* shared filter recovered from two input pulses (1e-3); -* held-out predicted within the noise (chi2/pt < 1.5); -* held-out the model cannot predict is flagged; -* per-condition resolutions recovered (3 %); -* slope of a nonlinear phase recovered from five energies (1e-3) and an unseen energy predicted; -* all dependence kinds against their formulas (1e-12); -* a 5-knot surface linear in a condition variable recovered (0.01 on knots relative to the central one); -* the smoothness penalty reduces the surface error on 10 % noise (0.0228 to 0.0214, a 6 % effect; the test asserts only "smaller"); -* network constraints hold exactly for 50 random weight sets; -* network recovers a hidden saturating map. - -**Findings you should know about.** -1. A normalised spectrometer reading cannot fix the overall scale of a transmission surface. The surface test compares shapes. -2. The network on a hidden map `8 E^2/(E^2+1.44)` with 6 training energies and 3 units is off by 0.11 rad at a held-out 1.0 uJ (0.01 rad - at 2.2 uJ). At sigma = 1 % that is a chi2 per point of 3.6, so the test asserts the error of the map (0.15 rad), not chi2. -3. Information on a nonlinear-phase slope is **not** monotonic in energy: at 4 uJ (12 rad) the fringes are finer than the 0.2 nm - resolution and the linearised information is lower than at 1 uJ. -4. A zero-energy shot is the best way to shrink the slope's uncertainty in the synthetic lab. It pins the filter parameters the slope is - correlated with (the covariance has -1.5e-5 between the first filter parameter and the slope). -5. A tracker needs the expected drift in physical units. With the process noise at the fitted sigma (0.006 nm) a 0.5 nm shift of the - centre was followed only 17 % in three steps; with `process_sigma` 0.3 nm it followed in one step and flagged the drift. - -**F5 (gradients).** Every evaluation is a full simulation and the Jacobian costs one run per parameter and condition. The network test -needed 1050 runs. Whether linear elements can give analytic derivatives was not investigated; the docs say so. - -## 3. Twin plugin (`plugins/twin`) - -* C#: `TwinPlugin` registers the command `twin`; `TwinCommand` starts `python -m ultrafast_twin` (interpreter `ULTRAFAST_PYTHON`, else - `python3`/`python`) with the plugin's `python/` on `PYTHONPATH`. Wired into the solution, `CopyPlugins.targets`, `build/package.py` and the - test project reference. `python/tests/test_packaging.py` expects the new default list. -* Python module `ultrafast_twin`: `project.py` (layout, `init`, `fit`), `synthetic.py` (hidden-truth lab, linear or saturating), `twin.py` - (`Twin`: draws from the fit covariance, `predict` with bands, `whatif` with paired draws), `validate.py`, `design.py`, `track.py`, - `goals.py`, `dependence.py`, `paths.py`, `__main__.py` (`init fit validate predict design track check serve`). - -**Measured (by hand, not in tests).** -* `init` + `fit` on the synthetic lab: 1.6 s, 135 runs. Recovered FWHM 8.497 nm (truth 8.5), centre 1033.008 nm (1033), slope 2.9998 - rad/uJ (3.0). Held-out energies 1.4 uJ and 3.0 uJ (outside the training range): chi2 per point 0.99 and 0.90. -* Credible-band coverage inside 2 sigma: 94-98 % on all seven conditions (calibrated). -* Saturating lab: verdict `fits-training-only`; held-out chi2 per point 11.9 and 180. This is the acceptance case. -* Placement ranking: `kerr` = `end` (gain 1.49) > `filter` (1.34) > `input` (0.00). Benchmark `check --benchmark`: 3 of 3 pass. -* Drift tracker (16 members, `process_sigma` 0.3 nm): a centre shifted from 1033.0 to 1033.5 nm was followed in one update and flagged. - -**Validation rules** (stated in `validate.py`, not tuned): flagged if chi2/pt > 3 x max(1, training) or fewer than 80 % of points within -2 sigma of band + noise; marginal above 1.5 x; verdicts `unvalidated`, `fits-training-only`, `marginal`, `ok`. - -## 4. Not done, in the order I would do it - -1. **T6 MCP server** (`mcp.py`; `__main__ serve` already imports it). Design from advisor call 2, section 5. -2. **Run the real path**: `ultrafast twin init|fit|validate` from a clean shell. The dispatcher only puts the plugin folder on `PYTHONPATH`; - `ultrafast` and NumPy may not be importable from `python3` (Homebrew Python has no NumPy). Decide what it passes - (`ULTRAFAST_PYTHON`, `python/src` in a source checkout, `ULTRAFAST_PLUGIN_PATH`). Add a C# test that `twin` is registered and that a bad - `ULTRAFAST_PYTHON` returns exit code 2. Check whether `TestFramework.cs` needs a line for the twin assembly (S3 added one for `ao`). -3. **Python tests** `python/tests/test_twin_*.py`: linear lab `ok`; saturating lab `fits-training-only`; `DependenceModel` against the - per-condition `fitted` values the engine wrote; tracker follows a shifted centre; benchmark passes; the acceptance run end to end. -4. **Docs**: `docs/plugins/twin.md` (parameter tables, project layout, commands, validation rules, findings 3-5 above), - `plugins/twin/README.md`, `docs/plugins/index.md`, `docs/testing.md` rows, `CHANGELOG.md`, `PROJECT_STATUS.md` counts, - `plugins/CLAUDE.md` entry, `docs/architecture/`. Shared files are for the merging session; the lines to add are in section 6. -5. `/verify`, then commit. Also run the two Python test files that touch plugins after `build_engine.py`. - -## 5. T6 design agreed with the advisor (to implement) - -* Hand-written JSON-RPC 2.0 over newline-delimited stdio; no `mcp` dependency. **The `ultrafast[mcp]` extra is therefore not needed**; I did - not touch `python/pyproject.toml`. Check the current MCP spec for the `protocolVersion` string and message shapes before writing. - Refusals are results with `isError: true`; unknown methods give error -32601; notifications get no reply. -* Eight tools: `describe`, `validate`, `run`, `compare_to_measured`, `scan`, `fit`, `explain_warning`, `plot`. -* Engine work runs in a **persistent worker subprocess** with its own stdout (the .NET host may write to fd 1). On overrun the worker is - killed and respawned and the server keeps serving. A pre-flight estimate (runs x grid points) refuses up front. -* Memory: `DOTNET_GCHeapHardLimit` in the worker (verify it is honoured by hosted and AOT) plus caps on N and nx*ny*spectralSamples from the S2 - spike numbers (1920x1080 at K = 16 used 3.6 GB). Not `RLIMIT_AS`. -* Worker started with `ULTRAFAST_HOME` set to an empty private folder so `~/.ultrafast/plugins` is not loaded. -* `set` is narrowed: roots `pulse/`, `element:`, `instrument:` only; never `type`, `kind`, `measured`, `file`; values finite numbers or - identifier-pattern strings (placement needs `instrument:spec/at`). -* Before every engine call walk the final system and run JSON: refuse strings starting with `@` and any callback / python / expression / - user kind (get the list by grepping `Schema/BuiltIn*.cs`, not from memory); every file-valued string must resolve inside the workspace - and is rewritten to an absolute path. Applies to `twin.json` and the fitted files too. -* Names the server writes match `^[A-Za-z0-9_-]{1,40}$`. Remove the `--allow-expressions` flag from `__main__.py` (it is already there and - has nothing behind it). -* Refusal tests: an expression, an `@name`, a callbacks / fields argument, `../x.csv`, an absolute path, a symlink out of the workspace, a - call over its time budget (and the next call still works); `tools/list` returns eight tools; an unknown argument is rejected. -* Core hook to record for S2/S5: a plugin allow-list in the engine (the worker can only avoid user plugins through `ULTRAFAST_HOME`). - -## 6. Lines for the shared files (for the merging session) - -* CHANGELOG: fitting plugin: `fit.conditions`, held-out conditions, `perCondition`, `dependence` (constant, proportional, linear, polynomial, - power, Chebyshev, network), `fit.surfaces`, `fitted-conditions.json`, operator `nonlinearPhase`. New plugin `twin` with - `ultrafast twin` and the `ultrafast_twin` module. -* testing.md: `FittingConditionsTests` (15 tests; fitting across conditions, dependence, surfaces, network). -* PROJECT_STATUS: C# test count +15 (before the twin and MCP tests). -* mkdocs.yml / llms.txt: `plugins/twin.md` once written (it breaks nothing now because the page does not exist yet). -* capabilities.md, roadmap: digital twin, fitting across conditions. - -## 7. For S5 - -* The `DigitalTwin/` examples E10-E13 can be built today from `ultrafast_twin` and the synthetic lab (`init`, `fit`, `validate`, `design`, - `track`, `whatif`). E14 (agent design through MCP) needs T6. E15 (wavefront twin) needs a spatial template that does not exist; the - `wavefront` operator and `ultrafast_ao` are the ingredients. -* The synthetic lab's data come from `ultrafast_twin.synthetic.build_lab`; its truth is in constants at the top of that file. -* `docs/examples/DigitalTwin/transforms` (old example 26) was not touched. diff --git a/docs/design/notes/s5-release.md b/docs/design/notes/s5-release.md deleted file mode 100644 index de093b8..0000000 --- a/docs/design/notes/s5-release.md +++ /dev/null @@ -1,95 +0,0 @@ -# S5 Examples, docs, verification: handoff - -Branch `dev/2026-11-s5-examples` (from `dev/2026-11` at 098dc80), 2026-11-10. What was built, what was verified, what was not, and what is left of the -plan in [roadmap-ao-twin-2026-11.md](../roadmap-ao-twin-2026-11.md). - -## 1. State of the whole plan after S5 - -| part | state | -|---|---| -| S1 Python structure, plotting, grouped examples, Computing | done, merged (2026-11-09) | -| S2 core: `modulator`, in-memory maps, streaming, sparse sampling, tiles, fast path | done; GPU parity written, **never run on a device** | -| S3 `ao` plugin | done; not built: DM hysteresis / creep / segmented / bimorph, per-channel tile systems, FROG / d-scan as a loop metric, `ultrafast ao` CLI commands, TS3 (4f line with spatial chirp), an engine-side Shack-Hartmann instrument | -| S4 fitting across conditions (F1-F5) and the `twin` plugin (T1-T5, T7) | done; **T6 MCP server not built** | -| S5 examples, docs, verification | done as listed below; the items in section 3 are **not** done | - -## 2. What S5 delivered - -**New examples** (each: a README with the commands and the quoted run output, `make_figures.py`, a hero figure, an animation where an algorithm -iterates, an assertion of the key number; all run under `ULTRAFAST_STRICT_OPTIONS=1`): - -| group | example | key number checked | run time | -|---|---|---|---| -| AdaptiveOptics | `dm-correction` | Strehl 0.32 to 0.90 (zonal), 0.97 (modal); slope rms falls by 1 - gain per step | 9 s | -| AdaptiveOptics | `sensorless-correction` | modal search 0.94 in 36 runs, SPGD 0.95 in 240 | 13 s | -| AdaptiveOptics | `aberrated-high-power` | peak fluence 234 to 102 J/m², peak nonlinear phase 1.55 to 0.60 rad | 45 s | -| AdaptiveOptics | `slm-beam-shaping` | engine against the Fourier prediction of the written 8-bit phase: correlation 0.98-0.99; full 1920 x 1080 panel (`--full`): 91 % on target | 3 s (+ 17 s full) | -| AdaptiveOptics | `tiled-array` | N x peak (7.00, 61.0); piston 0.785 vs 0.787; delay cost within 2 % of the overlap formula; SPGD lock | 10 s | -| AdaptiveOptics | `temporal-shaping` | pulse spacings; GDD x bandwidth 555 vs 554 fs; Weiner side-pulse energy 0.0159 vs 0.0159; chirp found to -900 fs², -4018 fs³ | 1 s | -| AdaptiveOptics | `scatterer-focusing` | enhancement 47.5 against 50.0 (Rayleigh matrix, Gaussian beam) | 3 s | -| DigitalTwin | `build-the-twin` | hidden centre, width, slope recovered; held-out predicted; saturating lab flagged | 4 s | -| DigitalTwin | `explore-the-twin` | what-if bands; information-gain ranking; drift followed to 0.02 nm and flagged | 20 s | -| Fibres (new group) | `soliton-breathing` | N = 1 within 0.03 %; N = 3 back to 1.003 x at z0 = (π/2) L_D | 8 s | -| Fibres | `raman-soliton-shift` | rate against T0 has slope -4.3 (Gordon: -4); absolute rate 16-36 % above Gordon's formula (stated, not tuned away) | 35 s | -| Fibres | `spm-compression` | pure-SPM bandwidth growth 8.85 x = the formula; 300 fs to 88 fs after a compressor found from the peak power | 100 s | -| Wavefronts | `spatial-gain-thermal-lens` (existing): a new hero with the 2-D beam, wavefront, Zernike content and the beam behind the rod | - | - | - -**Code and docs:** `docs/examples/_viz.py` (shared figure helpers; excluded from the examples wheel); `Fibres/` group (README, `make_all.py`); -`python/tests/test_twin.py` (9 tests, the S4 gap); a C# test that `ultrafast twin` is registered and exits with 2 when the interpreter cannot be started; -`docs/plugins/twin.md`, `plugins/twin/README.md`; the nav, `llms.txt`, plugin index, plugin system page, capabilities, roadmaps, agents guide, testing rows, changelog -and status; the gallery (`build/gallery.py` knows `Fibres`). - -**Findings made while writing the examples** (all stated in the READMEs): - -* The engine's focal plane equals the hologram's FFT lattice at `f = N dx² / λ`; patterns must stay in the inner half of the window and the prediction needs the - |sinc|² of the square pixel (correlation 0.90 without it, 0.98-0.99 with it). -* The SPGD gain matters: gain 1.5 gave 0.64, gain 3 gave 0.95, gain 5 gave 0.80 in the same 80 iterations on the sensorless example (the example uses 3). -* Tiled-array SPGD ends at 1.02 of the in-phase reference on the 128-point grid: not explained, within the example's bound. -* Pure SPM broadening is exactly the analytic formula (8.85 x); with the fibre's dispersion it is 4.05 x. -* `ultrafast twin` works from the real CLI when `ULTRAFAST_PYTHON` points to a Python with NumPy and `ultrafast` (checked: `init`, `fit`, `validate` on the synthetic lab). -* The wavefront sensor reports phase noise outside the beam (lenslets without light): maps are masked inside 1.5 beam radii (aberrated-high-power). - -## 3. Not done (the owner decides the order) - -1. **MCP server** (T6) and therefore the agent-driven design example (E14). The design is in `s4-twin.md` section 5; `ultrafast twin serve` exits with 2. -2. **E1 component gallery** and **E6 complex-field shaping with a phase-only device** (double-phase / Bolduc through an SLM) for the `ao` plugin. -3. **E13 spline against network transform** and **E15 wavefront twin** (needs a spatial project template). -4. **The older examples.** The owner's note asks that every example tells a story with plots and animations. Only `Wavefronts/spatial-gain-thermal-lens` was redone - here. Still the generic overview hero: all other examples in `GettingStarted`, `LaserSystems` (wavefronts, contrast, FROG trace at the end of each chain), - `Nonlinear`, `Diffraction`, `Branched`, `Diagnostics`, `Components`, `Wavefronts/position-dependent-index`, `DigitalTwin/transforms`. The pattern to follow is - in the new examples (a physics module that prints and asserts, a `make_figures.py` that draws the story). -5. Core requests (S2's area): `UF_WITH_AO` in the native library; a grid-planner hook for a shaper's side pulses and large programmed GDD; the unknown-element - message naming `ao`. -6. Not run anywhere: GPU parity (CUDA, OpenCL), OpticStudio, **net48 tests** (no Mono on this Mac; CI runs them), NativeAOT with the `ao` plugin. -7. Untested items listed in `s3-ao.md` (grating-derived dispersion of the shaper against a Littrow reference, end-to-end Bolduc, lobe fractions beyond 0 < f < 1). - -## 4. Verification - -Local run, macOS arm64, .NET 10, Python 3.12, 2026-11-10 (CI is the check for Linux, Windows and net48): - -* `dotnet build UltrafastEngine.sln -c Release`: 0 errors. -* C# `--slow`: 543 passed, 0 failed, 9 skipped before the new twin test; `PluginTests` (8, with the new one) pass: 544. -* Python (hosted engine, `ULTRAFAST_SLOW=1`, pyNLO): 216 passed, 1 skipped. Run **without** `ULTRAFAST_STRICT_OPTIONS`: with it, `test_zernike.py::test_python_fit_agrees_with_the_engines_wavefront_sensor` fails because the test writes the snake_case key `reference_wavelength_m` (a warning normally, an error in strict mode; a one-key fix in that test, not done here). -* `example_check.py` on AdaptiveOptics, DigitalTwin, Fibres, Wavefronts: all ok after one fix (a timing printed in `build-the-twin`). -* `mkdocs build --strict`: 0 warnings. Validation matrix regenerated. -* Not run: net48 tests, GPU, OpticStudio, the full `example_check` of the other groups (unchanged), the physics-reviewer agent on the new examples. - -## 5. Session log (2026-11-10, S5) - -1. Read the roadmap and the S1-S4 notes; wrote the done / to-do summary (sections 1-3 above). Advisor call 1 (before the examples): check CI, find how CI runs examples - (hosted engine, so `ao` is available), close the S4 gaps first, do not duplicate the Dudley PCF supercontinuum. -2. Built `docs/examples/_viz.py`, then the examples in the order DM correction, SLM shaping (incl. the full panel), sensorless, tiled array, temporal shaping, - scatterer, aberrated high-power beam, the two twin examples, the three fibre examples, and a new Wavefronts hero. Every physics claim was run first and the - README quotes the real output; disagreements are stated (Raman rate 16-36 % above Gordon's formula; engine against Fourier prediction 0.98-0.99, not 0.999). -3. Closed S4 gaps: `python/tests/test_twin.py` (9 tests), a C# test for the `twin` command, `ultrafast twin init|fit|validate` run from the real CLI, `docs/plugins/twin.md`. - Decided **not** to build the MCP server (T6) and E14 in this session; recorded in sections 1 and 3. -4. Merge wave: changelog, testing rows, status counts, README badges (544 C#, 216 Python), nav, `llms.txt`, plugin index and system page, capabilities, roadmaps, - agents guide, gallery (`Fibres`), validation matrix. -5. Verification: see section 4. **Advisor call 2 (before the merge)** found three things, all fixed: the README commands ran `make_figures.py` in CI (now marked - `# optional:` so the check runs only the physics script), the `test_zernike.py` key (`referenceWavelengthM`), and the roadmap status rows. -6. CI on `dev/2026-11` after the S5 merge: every job green except "Docs and example check", which failed on a wall time printed by `tiled_array.py` (the quoted - output must not contain timings; `build-the-twin` had the same problem earlier). Fixed by removing the time from the output; the next run was started. -7. Merged to `main` on the owner's instruction after that fix. No tag, no release, no version bump (2.3.0 is the convention when the owner asks). - -**Rule learned:** a script whose output is quoted in a README must print no wall time, and `example_check` runs every line of a ```bash block, so figure scripts -need `# optional:`. diff --git a/docs/design/notes/spike-scripts/spike_chromatic.py b/docs/design/notes/spike-scripts/spike_chromatic.py deleted file mode 100644 index 37eca4c..0000000 --- a/docs/design/notes/spike-scripts/spike_chromatic.py +++ /dev/null @@ -1,37 +0,0 @@ -"""S2 spike 3 (chromatic): a blazed grating as a wrapped-phase `mask file` and as a thin `bulk` with a sawtooth indexProfile (vacuum). -Reference: order m leaves at sin(theta) = m lambda / p (so the first order moves with lambda) and the blaze efficiency is -sinc^2(lambda0/lambda - 1). Runs cw at three wavelengths (a pulse is the incoherent sum over these colours for a linear element).""" -import os, sys, tempfile -import numpy as np -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "python", "src")) -import ultrafast as uf -from ultrafast import build as b - -n, ny, pitch, period, lam0, f_cam, w = 512, 8, 4e-6, 32e-6, 800e-9, 0.2, 0.6e-3 -x = (np.arange(n) - n / 2) * pitch; y = (np.arange(ny) - ny / 2) * pitch -d = tempfile.mkdtemp(); fm = os.path.join(d, "m.npz") -saw = np.mod(x / period, 1.0) # 0..1 per period -np.savez(fm, x_m=x, y_m=y, amplitude=np.ones((n, ny)), phase_rad=np.repeat((2 * np.pi * saw)[:, None], ny, 1)) -L = 10e-6; dn0 = lam0 / L # OPD depth = lam0 across one period -def tr(ip=None): return b.transverse("cartesian", nx=n, ny=ny, half_width_m=n * pitch / 2, spectral_samples=1, initial_profile=ip, halfWidthYM=ny * pitch / 2) -def sinc2(u): return np.sinc(u) ** 2 - -def far(system, lam): - r = uf.run(system, b.run_spec(b.cw(lam, 1.0), instruments=[b.instrument("camera", at="end", name="c", imaging={"plane": "far", "focalLengthM": f_cam}, - sensor={"pixels": [1024, 1], "pixelSizeM": 20e-6})]), - fields={"saw": lambda X, Y, Z: dn0 * np.mod(X / period, 1.0)}) - v = r.instrument("c"); m = np.array(v["fluence_j_m2"]).sum(1); xs = np.array(v["x_m"]) - return xs, m - -print("lambda_nm order1 x (mm) predicted eff mask eff index predicted eff") -for lam in (760e-9, 800e-9, 840e-9): - out = [] - for kind in ("mask", "index"): - els = [b.gap("in", 0.0, medium="Vacuum", transverse=tr(b.initial_profile("gaussian", radius_m=w)))] - els += [b.mask("g", "file", file=fm, design_wavelength_m=lam0)] if kind == "mask" else [b.bulk("g", "Vacuum", L, transverse=tr(), indexProfile="@saw")] - els += [b.gap("p", 0.0, medium="Vacuum", transverse=tr())] - xs, m = far(b.system("s", els), lam) - side = xs > 2.5e-3 - x1 = (xs[side] * m[side]).sum() / m[side].sum() - out.append((x1, m[side].sum() / m.sum())) - print(f"{lam*1e9:6.0f} {out[0][0]*1e3:8.3f} / {out[1][0]*1e3:8.3f} {f_cam*lam/period*1e3:8.3f} {out[0][1]:.4f} {out[1][1]:.4f} {sinc2(lam0/lam-1):.4f}") diff --git a/docs/design/notes/spike-scripts/spike_loop.py b/docs/design/notes/spike-scripts/spike_loop.py deleted file mode 100644 index df5353e..0000000 --- a/docs/design/notes/spike-scripts/spike_loop.py +++ /dev/null @@ -1,23 +0,0 @@ -"""S2 spike 4 (loop): time per trial of a 128^2 cw spatial run: a new mask file per trial (today's only route) and a run with no mask (the floor).""" -import os, sys, time, tempfile -import numpy as np -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "python", "src")) -import ultrafast as uf -from ultrafast import build as b - -n, pitch, lam = 128, 20e-6, 800e-9 -x = (np.arange(n) - n / 2) * pitch -d = tempfile.mkdtemp() -tr = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * pitch / 2, spectral_samples=1, initial_profile=ip) -def trial(i, with_mask): - els = [b.gap("in", 0.0, medium="Vacuum", transverse=tr(b.initial_profile("gaussian", radius_m=0.4e-3)))] - if with_mask: - f = os.path.join(d, f"m{i}.npz"); np.savez(f, x_m=x, y_m=x, amplitude=np.ones((n, n)), phase_rad=np.random.default_rng(i).standard_normal((n, n)) * 0.1) - els.append(b.mask("m", "file", file=f, design_wavelength_m=lam)) - els.append(b.gap("p", 0.05, medium="Vacuum", transverse=tr())) - r = uf.run(b.system("s", els), b.run_spec(b.cw(lam, 1.0), instruments=[b.instrument("beamProfile", at="end", name="o")])) - return r -for wm in (False, True): - trial(0, wm); t = time.time() - for i in range(1, 21): trial(i, wm) - print(("with new mask file" if wm else "no mask "), f"{(time.time()-t)/20*1e3:.1f} ms per trial") diff --git a/docs/design/notes/spike-scripts/spike_scale.py b/docs/design/notes/spike-scripts/spike_scale.py deleted file mode 100644 index 23ee85c..0000000 --- a/docs/design/notes/spike-scripts/spike_scale.py +++ /dev/null @@ -1,25 +0,0 @@ -"""S2 spike 1 (scale): one cw frequency through a mask file, a gap and a beam profile on a large cartesian grid. -Usage: spike_scale.py NX NY [spectral_samples]. Prints wall time and peak RSS.""" -import os, sys, time, resource, tempfile -import numpy as np -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "python", "src")) -import ultrafast as uf -from ultrafast import build as b - -nx, ny = int(sys.argv[1]), int(sys.argv[2]); K = int(sys.argv[3]) if len(sys.argv) > 3 else 1 -pitch, lam = 8e-6, 800e-9 -x = (np.arange(nx) - nx / 2) * pitch; y = (np.arange(ny) - ny / 2) * pitch -X, Y = np.meshgrid(x, y, indexing="ij") -w = 2.0e-3 -phase = 2 * np.pi * 3 * np.sin(2 * np.pi * X / 1.0e-3) # some structure -d = tempfile.mkdtemp(); f = os.path.join(d, "m.npz") -np.savez(f, x_m=x, y_m=y, amplitude=np.ones((nx, ny)), phase_rad=phase) -tr = lambda ip=None: b.transverse("cartesian", nx=nx, ny=ny, half_width_m=nx * pitch / 2, spectral_samples=K, initial_profile=ip, halfWidthYM=ny * pitch / 2) -sysm = b.system("s", [b.gap("in", 0.0, medium="Vacuum", transverse=tr(b.initial_profile("gaussian", radius_m=w))), - b.mask("slm", "file", file=f, design_wavelength_m=lam), - b.gap("prop", 0.2, medium="Vacuum", transverse=tr())]) -t = time.time() -res = uf.run(sysm, b.run_spec(b.cw(lam, 1.0) if K == 1 else b.pulse(lam, 30e-15, energy=1e-6), instruments=[b.instrument("beamProfile", at="end", name="out")])) -dt = time.time() - t -m = np.array(res.instrument("out")["fluence_map_j_m2"]) -print(f"{nx}x{ny} K={K}: {dt:.2f} s, peak RSS {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/(2**30 if sys.platform=='darwin' else 2**20):.2f} GB, map {m.shape}, max {m.max():.3e}") diff --git a/docs/design/notes/spike-scripts/spike_sparse.py b/docs/design/notes/spike-scripts/spike_sparse.py deleted file mode 100644 index 9adfa51..0000000 --- a/docs/design/notes/spike-scripts/spike_sparse.py +++ /dev/null @@ -1,33 +0,0 @@ -"""S2 spike 2 (sparse spectral sampling): 800 nm / 30 fs through a phase mask, far field. K stored frequencies against a dense run. -Usage: spike_sparse.py PHASE_PEAK_RAD (peak phase of a smooth random screen, rad at the design wavelength)""" -import os, sys, time, tempfile -import numpy as np -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "python", "src")) -import ultrafast as uf -from ultrafast import build as b - -amp_rad = float(sys.argv[1]); n = 128; pitch = 20e-6; lam = 800e-9; w = 0.5e-3 -x = (np.arange(n) - n / 2) * pitch -X, Y = np.meshgrid(x, x, indexing="ij") -rng = np.random.default_rng(3) -k = np.fft.fftfreq(n, pitch); KX, KY = np.meshgrid(k, k, indexing="ij") -screen = np.fft.ifft2(np.fft.fft2(rng.standard_normal((n, n))) * np.exp(-(KX**2 + KY**2) / (2 * (400.0) ** 2))).real -screen *= amp_rad / np.abs(screen).max() -d = tempfile.mkdtemp(); f = os.path.join(d, "m.npz") -np.savez(f, x_m=x, y_m=x, amplitude=np.ones((n, n)), phase_rad=screen) - -def run(K): - tr = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * pitch / 2, spectral_samples=K, initial_profile=ip) - s = b.system("s", [b.gap("in", 0.0, medium="Vacuum", transverse=tr(b.initial_profile("gaussian", radius_m=w))), - b.mask("slm", "file", file=f, design_wavelength_m=lam), b.gap("p", 0.0, medium="Vacuum", transverse=tr())]) - t = time.time() - r = uf.run(s, b.run_spec(b.pulse(lam, 30e-15, energy=1e-6), instruments=[ - b.instrument("camera", at="end", name="focus", imaging={"plane": "far", "focalLengthM": 0.5}, sensor={"pixels": [n, n], "pixelSizeM": 20e-6})])) - v = r.instrument("focus") - return np.array(v["fluence_j_m2"]), time.time() - t - -ref, tref = run(96) -print(f"phase peak {amp_rad} rad; dense K=96: {tref:.1f} s; focal peak {ref.max():.4e}") -for K in (1, 2, 4, 8, 16, 32, 48): - m, t = run(K) - print(f"K={K:3d}: {t:5.1f} s peak ratio {m.max()/ref.max():.5f} rel L2 {np.linalg.norm(m-ref)/np.linalg.norm(ref):.2e}") diff --git a/docs/design/notes/spike-scripts/spike_stream.py b/docs/design/notes/spike-scripts/spike_stream.py deleted file mode 100644 index 7993026..0000000 --- a/docs/design/notes/spike-scripts/spike_stream.py +++ /dev/null @@ -1,25 +0,0 @@ -"""S2 streaming check (as spike 1): one cw frequency through a mask file, a gap and a beam profile on a large cartesian grid. -Usage: spike_stream.py NX NY K BLOCK KEEP (transverse.frequencyBlock, 0 = all at once; outputs.keepProfiles all | used). Prints wall time and peak RSS.""" -import os, sys, time, resource, tempfile -import numpy as np -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "python", "src")) -import ultrafast as uf -from ultrafast import build as b - -nx, ny = int(sys.argv[1]), int(sys.argv[2]); K = int(sys.argv[3]); B = int(sys.argv[4]); KEEP = sys.argv[5] -pitch, lam = 8e-6, 800e-9 -x = (np.arange(nx) - nx / 2) * pitch; y = (np.arange(ny) - ny / 2) * pitch -X, Y = np.meshgrid(x, y, indexing="ij") -w = 2.0e-3 -phase = 2 * np.pi * 3 * np.sin(2 * np.pi * X / 1.0e-3) # some structure -d = tempfile.mkdtemp(); f = os.path.join(d, "m.npz") -np.savez(f, x_m=x, y_m=y, amplitude=np.ones((nx, ny)), phase_rad=phase) -tr = lambda ip=None: b.transverse("cartesian", nx=nx, ny=ny, half_width_m=nx * pitch / 2, spectral_samples=K, initial_profile=ip, halfWidthYM=ny * pitch / 2, frequencyBlock=B) -sysm = b.system("s", [b.gap("in", 0.0, medium="Vacuum", transverse=tr(b.initial_profile("gaussian", radius_m=w))), - b.mask("slm", "file", file=f, design_wavelength_m=lam), - b.gap("prop", 0.2, medium="Vacuum", transverse=tr())]) -t = time.time() -res = uf.run(sysm, b.run_spec(b.cw(lam, 1.0) if K == 1 else b.pulse(lam, 30e-15, energy=1e-6), instruments=[b.instrument("beamProfile", at="end", name="out")], outputs={"keepProfiles": KEEP})) -dt = time.time() - t -m = np.array(res.instrument("out")["fluence_map_j_m2"]) -print(f"{nx}x{ny} K={K} block={B} keep={KEEP}: {dt:.2f} s, peak RSS {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/(2**30 if sys.platform=='darwin' else 2**20):.2f} GB, map {m.shape}, max {m.max():.3e}") diff --git a/docs/design/notes/spikes.md b/docs/design/notes/spikes.md deleted file mode 100644 index 3d6ea61..0000000 --- a/docs/design/notes/spikes.md +++ /dev/null @@ -1,116 +0,0 @@ -# S2 spikes: measurements that decide the core design - -Run 2026-11-10 on the owner's Mac (Apple silicon, 8 cores, 16 GB, CPU back-end, .NET 10). Scripts: `docs/design/notes/spike-scripts/` -(run with the repo `.venv` and `ULTRAFAST_LIBRARY` set to the hosted library, see `verification-environment`). - -**Gate G0 verdict: no spike contradicts the plan; one retires its biggest risk (scale).** The owner should still read the -decisions in section 8 (updated 2026-11-10 after the completion pass: streaming, the `index` rule and the fast-path fix). - -## 1. S1-scale: a full SLM panel (`spike_scale.py`) - -Gaussian beam (w = 2 mm) through a `mask file` (phase map, 8 µm pitch) and a 0.2 m gap, read by `beamProfile`. - -| grid | stored frequencies K | wall time | peak memory | -|---|---|---|---| -| 512 x 512 | 1 (cw) | 0.6 s | 0.20 GB | -| **1920 x 1080** | 1 (cw) | **3.3 s** | **1.05 GB** | -| **1920 x 1080** | 8 (30 fs pulse) | 15.7 s | 1.74 GB | -| **1920 x 1080** | 16 (30 fs pulse) | **32.1 s** | **3.6 GB** | -| 4096 x 4096 | 1 (cw) | 19.9 s | 3.4 GB | - -Time is linear in K (about 2 s per frequency), memory grows by about 0.23 GB per frequency at 1920 x 1080. **A full 1920 x 1080 -panel with a 30 fs pulse fits a 16 GB laptop in half a minute at K = 16.** Only a 4096 x 4096 padded grid with K >= 16 -(estimated 8-13 GB) needs streaming. - -## 2. S2-sparse: how many stored frequencies (`spike_sparse.py`) - -800 nm / 30 fs, 128 x 128, a smooth random phase screen (peak 6, 30, 100 rad at the design wavelength, so the phase scales with -frequency: the harshest chromatic case), far-field camera. Error = relative L2 difference of the focal-plane fluence map against a -dense run with K = 96. - -| K | 6 rad | 30 rad | 100 rad | -|---|---|---|---| -| 1 | 8.4e-2 | 1.3e-1 | 8.4e-2 | -| 4 | 1.4e-3 | 2.0e-2 | 4.6e-2 | -| 8 | 1.7e-4 | 2.0e-3 | 2.8e-3 | -| 16 | 7.9e-5 | 1.6e-4 | 2.0e-3 | -| 32 | 1.1e-4 | 3.0e-4 | 4.2e-4 | -| 48 | 8.7e-4 | 1.6e-3 | 1.6e-3 | - -* **Sparse spectral sampling already exists**: it is `transverse.spectralSamples` (default 48, profile interpolated between stored - frequencies). No new sampler is needed; what is missing is an error estimate. -* K = 8 to 16 gives 1e-3 or better up to 30 rad of phase. At 100 rad (16 waves) K >= 32 is needed. -* The error is **not monotone**: K = 48 is worse than K = 16. The reference (K = 96) is itself a sampled run, and the bin edges of - the 48- and 96-sample sets differ, so differences near 1e-3 are the floor of this comparison, not a measurement of K = 48. -* Rule of thumb that fits the table: the stored frequencies must resolve the phase change across the band, - K >= 2 x (peak phase in waves x relative bandwidth x number of waves of spread) -- stated in the docs as "compare K and 2K". - -## 3. S3-chromatic: is a wrapped phase map a grating? (`spike_chromatic.py`) - -A blazed grating (period 32 µm, sawtooth optical path of depth 800 nm) as (a) a `mask file` holding the **wrapped** phase at -800 nm and (b) a 10 µm `bulk` of vacuum with a sawtooth `indexProfile`. cw at three wavelengths; first-order position in the -far field (f = 0.2 m) and first-order efficiency: - -| lambda | position, mask / index / theory f lambda / p | efficiency mask / index / theory sinc^2(lambda0/lambda - 1) | -|---|---|---| -| 760 nm | 4.802 / 4.767 / 4.750 mm | 0.949 / 0.996 / 0.991 | -| 800 nm | 5.018 / 5.001 / 5.000 mm | 0.922 / 0.999 / 1.000 | -| 840 nm | 5.250 / 5.250 / 5.250 mm | 0.916 / 0.996 / 0.993 | - -* **Both routes disperse like a grating (position proportional to lambda) and follow the blaze curve.** The existing `mask file` - rule (phase_rad at the design wavelength, scaled by omega / omega_design) is the right model for a wrapped phase, **provided the - file holds the wrapped phase**: the wrap is then a sawtooth in optical path, which is what a blazed surface is. -* The mask is 3-8 % less efficient than theory: its sub-pixel average over a cell (6 x 6 points) is a real pixel-integration effect at - 8 samples per period (sinc^2(1/8) = 0.95); the point-sampled index profile has none. A `modulator` must therefore state its sampling - rule (average over the cell, with the fill factor) rather than leave it to the grid. -* The centroid positions have ~0.7 % scatter from the order's finite width (centroid over an asymmetric window), not from the model. -* A phase that does **not** scale with frequency (geometric / Pancharatnam-Berry, a mirror's tilt as pure phase) is not covered by - `mask file`; a liquid-crystal cell needs phase = 2 pi dn(lambda) d / lambda. These are the three chromatic rules of section 8. - -## 4. S4-loop: cost per trial (`spike_loop.py`) - -128 x 128, one frequency, mask, gap, `beamProfile`, through the Python API: - -| | time per trial | -|---|---| -| no mask | 10 ms | -| a new `mask file` per trial (np.savez + read + 6 x 6 cell average) | 46 ms | - -A loop of 1000 trials costs 46 s today and would cost about 10-15 s with an in-memory map. The compile cache is worth a further -saving only if compile dominates the 10 ms floor (not measured separately); the in-memory map is the main gain (4x). - -## 5. S5-tiles: not measured as a Branched system - -Seven tiles as a Branched system against a coherent sum was **not** run: a Branched system recombines arms with a Gaussian mode -overlap at one plane (docs/reference/branched.md), so it cannot give the cartesian far field of an array. The tile source -(C4) is therefore tested against the analytic array factor instead (N^2 peak, exp(-sigma^2), lambda / pitch lobes). - -## 6. S6-multi-condition fit (for S4, not built here) - -`Fitter.Run(system, run, ...)` in `plugins/fitting/src/Fitting.cs` takes **one** system and **one** run spec; the data are the -`measured` blocks of that run's instruments, and the cost is the stacked residual over them. A joint fit over several operating -conditions (energies, angles) therefore needs a new level above `Run`: a list of (run overrides, instrument data) sharing the -fitted parameters, evaluated one run per condition per cost call. The Jacobian machinery (`Eval`, `PseudoInverse`, -`report.Warnings` for undetermined parameters) works on stacked residuals and can be reused. Held-out conditions are a flag on the list. - -## 7. S7-repetition (for the Pockels comment) - -* `repRateHz` (on the pulse or on a gain / amplifier element) switches on the steady-state iteration in - `Gain/GainElements.cs` (lines ~361 and ~529, `SteadyState`, `MaxPulses`, `Tolerance`): a fast energy model over pulses, then the full - model for the last one. This confirms the plan: repetition is a property of the gain model only. -* `grep -i pockels|pulsePicker` over `src/` finds nothing: **there is no switch model** (extinction ratio, edges, leakage). The - S6 sketch in the roadmap stands. - -## 8. Decisions (as built) - -| topic | decision | alternatives rejected | -|---|---|---| -| Streaming over frequency (C3) | **Built in the linear spatial propagator** (`transverse.frequencyBlock`: blocks of stored frequencies, exact) plus **`outputs.keepProfiles: "used"`** (drop the profiles no instrument reads). Measured (`spike_stream.py`, 1920 x 1080, K = 16): the process peak stays 2.7-3.4 GB for block 0, 1, 4 -- the .NET GC returns freed large arrays late, so the peak is not the live set. The live set after a five-element 1024 x 1024 x 16 run falls 1587 -> 336 MB with `used`. 1920 x 1080 at K = 16 already fits (32 s / 3.6 GB); a padded 4096 x 4096 grid with K >= 16 needs `used` plus enough RAM for one profile (4.3 GB at K = 16) | Streaming the whole executor (instruments accumulating per frequency): the largest change in the executor, for a case no stated requirement needs | -| Sparse sampling (C3) | **Reuse `transverse.spectralSamples`.** The modulator reports `spectralSamplingError` (largest distance of a stored sample from the line through its neighbours; about 1/K^2, measured 1.43, 0.45, 0.10, 0.032 at K = 4, 8, 16, 32 for 40 rad) and warns above 0.05. This is a *conservative proxy* (it skips a whole bin), not the true error: for the same screen the true error of the focal map at K = 16 was 1.6e-4 (30 rad, spike 2). It is blind to phase steps near a multiple of 2 pi per stored frequency, so a second warning fires when the step exceeds pi/2. Rule in the docs: compare K and 2K. | A new sampler; adaptive frequency selection (no evidence it beats uniform bins) | -| Chromatic rules | Three values. `opd` (default): the written phase is an optical path, `phi(omega) = phi_design omega / omega_design`; a **wrapped** map is therefore a real grating (spike 3). `fixedPhase`: `phi(omega) = phi_design` (geometric phase). The plan's `phaseAtDesign` (a phase given at the design wavelength and scaled with omega) is accepted as an alias of `opd`. `index`: `phi(omega) = phi_design omega dn(omega) / (omega_design dn(omega_design))` with dn from `deltaIndexCauchy` or a `deltaIndex` table -- a liquid-crystal cell. Added after S2's first pass: a wrapped `opdM` from the plugin is right at one wavelength only, because a dispersive dn makes the phase non-proportional to omega, and S3 cannot add core code. The input may be `phaseRad` at a design wavelength or `opdM` directly. | A "wrap inside the core" mode: would hide the physics; the plugin wraps the way the device does. A built-in liquid-crystal catalogue: device data, belongs to the plugin (it passes the dn coefficients) | -| Sampling | The simulation cell gets the average of the device over the cell (6 x 6 sub-samples, so edge weights come in steps of 1/6; a fast path when the whole cell is in one active device cell). `fillFactor`, gaps, `crosstalkSigmaCells` (Gaussian blur of the written phase and amplitude) or an explicit `crosstalkKernel` (the roadmap's name). | Point sampling (spike 3 shows the pixel effect is real) | -| In-memory maps (C2) | Array in the spec (Python converts numpy), **callback `"@name"`** at the cell centres (no file, no JSON; C API `uf_run_set_field_callback`), or NPZ file. Every run compiles and reads the callback then, so a loop gets the new map each trial (tested: N trials = N file runs, 1e-12). A compile cache is **not** built: the floor of a 128^2 trial is 10 ms including the compile, and the file map was the cost (46 ms). | A C-API-only route (Python could not use it). A compile cache keyed on the system: saves part of a 10 ms floor and must invalidate on every map change | -| Tiles (C4) | `initialProfile.kind = "tiles"` in the transverse code: complex sum of Gaussian / tophat tiles with piston, tilt, delay per tile, normalised to unit energy per stored frequency. | A Branched system (spike 5: it recombines arms by a Gaussian mode overlap at one plane and cannot give an array's far field) | -| Zonal outputs (C5) | `wavefrontSensor` gains `slope_x_rad` and `slope_y_rad` [x][y]; the wavefront map was already a stable array. | | -| GPU parity (C6) | **Test written, skipped here**: `GpuTests.TilesAndModulator_IntoANonlinearPlate_MatchTheCpu` (tiles + modulator + a Kerr plate on the device = CPU, 1e-9). This Mac's OpenCL has no double precision and PoCL has no macOS wheel; the test body was run CPU against CPU. The modulator and tile source run on the CPU; the device back-end runs the nonlinear spatial propagation they feed. Run it on a machine with a CUDA / FP64 OpenCL device or PoCL on Linux. | | -| Fast path (C5) | cw and narrow-band runs use `spectralSamples: 1`. Found and fixed: one stored frequency was the **low edge of the band** (3e-6 off the carrier for a 1 ns pulse); it is now the energy centroid. A cw source's bin is snapped to its grid lattice (1.2e-6 off at 1030 nm), so the roadmap's "cw = pulse at the carrier, 1e-9" holds for the stored frequency (1e-7, one bin of a 1 ns grid) and the patterns agree to 3e-6 | Moving the cw line onto the exact carrier: changes every cw result in the repository for a 1e-6 effect | diff --git a/docs/design/plan-ao-digital-twin-2026-11.md b/docs/design/plan-ao-digital-twin-2026-11.md deleted file mode 100644 index 3c3b2b0..0000000 --- a/docs/design/plan-ao-digital-twin-2026-11.md +++ /dev/null @@ -1,441 +0,0 @@ -# Assessment: test fixes, an adaptive-optics plugin, and the digital-twin use case - -Written 2026-11-09 after an ultra-high-intensity laser conference. **Nothing here is implemented.** It says what the engine -does today, what is missing, what examples to add and how useful each item would be to the wider laser community. Sizes are -focused agent-days (S ≈ 1, M ≈ 2-3, L ≈ 4-6, XL ≥ 7), as in [plan-next.md](plan-next.md). - -How sure each statement is: *checked* means I read the docs or code for it; *not checked* means it is an inference that a -spike must confirm before anything is built on it. - -## Owner decisions (2026-11-09, answers to the first version of this page) - -| # | decision | effect on this page | -|---|---|---| -| 1 | The AO plugin is about **transverse** shaping first; **temporal** shaping is wanted too | B6 rewritten: a temporal-shaping comment and a staged plan (a 1-D spectral shaper, an AOPDF model) | -| 2 | "Zernike" means Zernike **modes** | confirmed; modal correction is a first-class path next to zonal | -| 3 | The fibre array is a **coherent tiled-aperture amplifier array** (separate channels, one phase actuator each) | B3: `fibreBundle` becomes `tiledArray`; multicore and imaging bundles are out of scope | -| 4 | No specific vendor device. Everything is **parametric**: dimensions, grid, pitch, count, response | B3: presets are generic, every dimension is a parameter; no vendor data needed | -| 5 | Scale: up to **what exists physically, a full 1920 × 1080 SLM** | B8 and B9: a hard requirement that sets the first milestone (streaming per frequency, sparse spectral sampling, GPU) | -| 6 | The digital twin becomes **its own plugin on top of fitting**; the **fitting plugin** gets multi-condition transforms (PINN-style or other), for example spectra at different energies | C2 and C3 rewritten; new items F1-F4 | -| 7 | An **MCP** server is of interest; explain the trade-offs | new section C6 | -| 8 | `paper-replicator` runs on `haiku` (its usage was very high) | done in `.claude/agents/paper-replicator.md`; separate from this plan | - -Second round (2026-11-09, after the first roadmap): - -| # | decision | effect | -|---|---|---| -| 9 | Condense into **4-5 Claude Code sessions** to save tokens; choose a model and advisor per session | the roadmap is now five session briefs (S1-S5) with a model and advisor table | -| 10 | Plotting: **consolidate** the existing plotting code, add **animation**, add a **Zernike decomposition plot** (may need core work) | S1; *checked:* the core already has `Zernike.Fit`, so only a numpy port plus a cross-check test is needed | -| 11 | **Python package structure**: `ultrafast.algorithms`, `.system` (Sequential, Branched), `.components` (BeamSplitter, LeakyMirror, ...) and more | S1, roadmap section 4.1; lowercase module names recommended over the capitalised form | -| 12 | CI: macOS is not available as a runner; run the tests on **Linux** | done in the planning session: `macos-14` replaced by `ubuntu-24.04-arm` | -| 13 | **Group the examples** (Computing, Wavefronts, Diffraction, Branched, Diagnostics, LaserSystems, ...); an installable `ultrafast[examples]`; **optical-computing examples** with processing time against theory | S1, roadmap sections 4.3-4.4; a pip extra installs dependencies, not folders, so it needs a separate `ultrafast-examples` package | -| 14 | A **run-all script** per example (and per group) that writes the figures and animations; the owner runs it locally, not in CI | S1, roadmap section 4.3 | -| 15 | A **logo from code**, licence with attribution and a citation request, contact email | done in the planning session (Apache-2.0 + NOTICE; the owner chose this over MIT) | -| 16 | **Pockels cells** and repetition: comment in the plan | roadmap section 6 (later session S6) | - -The work packages, their order and their acceptance criteria are in -[roadmap-ao-twin-2026-11.md](roadmap-ao-twin-2026-11.md). - -## Summary - -| part | verdict | -|---|---| -| **A. two failing CI tests** | Both are CI-versus-Mac numerical sensitivity, not new physics bugs. One is a README that quotes round-off noise (easy). One is a retrieval test that finds a worse minimum on Linux (needs a look at the algorithm). | -| **B. adaptive optics** | About 40 % of the physics exists (Zernike wavefronts, file phase masks, spatial propagation, a wavefront sensor, Gerchberg-Saxton, GRIN-style `indexProfile`). Missing are the *device models* (actuators, pixels, voltages, wrapping, fibre cores), a real sensor model, the calibration / control algorithms and a fast loop. Recommended split: a few general primitives in the core, devices and algorithms in an `ao` plugin. | -| **C. digital twin** | Mostly supported today (transform blocks, measured data, fits, backward propagation, `lab-mirror` agent). The gaps are packaging (one clear worked example), joint fitting over many operating conditions (not checked), and a gradient path for neural-network transforms. The strongest differentiator of the three. | - ---- - -## A. Test fixes - -Both failed in the CI run of commit `d283409` (467 passed, 1 failed on Linux; the docs job found the second). Both pass on the -owner's Mac, which is the clue. - -### A1. `Frog_PcgpaAndEpie_RecoverAChirpedPulse_FromSdAndPgTraces_WithTheSign` - -* Failing case: SD-FROG, ePIE, GDD −300 fs². Retrieved duration 34.4 fs, true 41.6 fs (17 % off, tolerance 3 %). -* *Checked:* the test asserts `res.Error < 8e-3` first and that passed, so the retrieved pulse **reproduces the trace**, and - its duration is still 17 % off. That is a different pulse that fits the trace equally well: the usual FROG ambiguity - (time reversal, or a near-degenerate minimum), here for the case with negative chirp. -* *Not checked:* why it differs from the Mac. Suspects, in order: (1) the ePIE start or visiting order depends on something - that round-off changes (FFT provider: Accelerate on the Mac, a managed or FFTW one on Linux); (2) the 300-iteration limit - is only just enough on this case; (3) a random start with a seed that is not fixed (I did not find `seed` or `Random` in - `Retrieval/Retrieval.cs`, so check how the initial guess is built). -* Proposed order of work: reproduce on Linux (Docker, `linux/amd64`, or `ULTRAFAST_FFT=managed` on the Mac if that switch - exists), print duration versus iteration, then decide. If the algorithm is marginal, fix the **algorithm** (more - iterations are not a fix; a better initial guess or step schedule is). Do not loosen the 3 % tolerance. If the - degenerate minimum is a real ambiguity of SD-FROG, say so in `docs/reference/retrieval.md` and test the property the - retrieval can guarantee (trace error and the *sign* of the GDD), keeping duration for the cases where it is unique. -* Size: S to M. - -### A2. `32-interferometers`: `4.1e-14` versus `4.0e-14` - -* *Checked:* the script prints `np.abs(s_out - s_in * fringes)[band].max() / s_in.max()` with `%.1e`. A relative error of - 4e-14 is double-precision round-off, which depends on the FFT implementation and the CPU. Each platform gives a different - last digit, so the README can never match on all of them. -* Fix (the number is a *claim of exactness*, not a measurement): print a bound, for example - `"spectral fringes largest error < 1e-12 of the peak"` using `max(err, ...)` rounding, and change the README line to match. - Do the same for any other quoted number that is round-off (`grep` the example READMEs for `e-1[0-9]`). - `build/example_check.py` could also learn a rule: numbers below 1e-10 are compared as "both below a threshold". -* Size: S. - ---- - -## B. Adaptive optics (AO) - -### B1. What the engine already does (all *checked* in the docs) - -| need | what exists | limit | -|---|---|---| -| wavefront error | `wavefront` element and `aberration` on mirrors, surfaces, lenses: Zernike (Noll, waves rms) or random figure; OPD applied as phase ωW/c on the carried profile; Maréchal Strehl estimate | static; Zernike or random only; radial grids keep only axisymmetric terms | -| arbitrary phase / amplitude map | `mask` of kind `file`: NPZ with `x_m`, `y_m`, `amplitude`, `phase_rad` (phase scales with frequency like an optical path) | a file, not a device; no pixels, voltages, wrapping rule or fill factor; slow to loop over (file I/O per trial) | -| refractive-index map | `indexProfile`, `absorptionProfile`, `n2Scale` as expressions, tables or Python callbacks of (x, y, z, r, θ) on `bulk` (example 16, a GRIN rod focuses as the thin-lens formula, 0.9 %) | an SLM written as a thin `bulk` with Δn(x, y) should work; *not checked* for 10⁶-pixel maps or for wrapped phase | -| spatial propagation | radial and cartesian grids, exact or paraxial diffraction, Fresnel-integral checks < 1 %, GPU back-end for spatial envelopes | memory and time grow as N_x · N_y · N_ω (the profile is carried at every stored frequency) | -| wavefront sensing | `wavefrontSensor` instrument (lenslets, wavefront map in waves, Zernike fit to 36 terms, rms / PV, defocus, astigmatism, coma, spherical); `camera` with imaging and far field; virtual `field` and `spaceTime` probes | an *ideal* sensor: no spot centroiding, noise, dynamic range or lenslet cross-talk | -| phase retrieval | Gerchberg-Saxton from near- and far-field intensities (`retrieve gerchbergSaxton`, with restarts), output ready for `initialProfile` or a `file` mask | one algorithm, no target-shaping variants | -| optimisation | scans, Nelder-Mead, Sobol sensitivities, fitting-plugin LM, CMA-ES and MCMC; `transform` `wavefront` operator with fittable Zernike terms | tens of parameters, not hundreds of actuators | -| beam splitting and recombination | Branched systems recombine arms with the carrier phase and a Gaussian mode overlap | overlap is simple Gaussian at one plane; no array of N coherent cores | -| temporal side | spectral phase and amplitude `transform` blocks, FROG, d-scan, chirp-sensitive instruments | no spatial light modulator in a Fourier plane (a pulse *shaper*) | - -So a user can already say "put this phase map in the beam and look at the focus". They cannot yet say "this DM with 97 -actuators, these influence functions, this stroke, drive it so the Strehl is maximal". - -### B2. What is missing - -1. **Device models** (the heart of the request): deformable mirror, SLM, fibre bundle / multicore array. -2. **A programmable map primitive** that takes the map as an in-memory array (run spec or Python), applies it with sub-pixel - averaging and a stated *chromatic rule*, and is cheap to call thousands of times. -3. **A physical wavefront-sensor model**: Shack-Hartmann with centroiding and noise, plus a modal/zonal reconstructor; - optionally a lateral-shearing or phase-diversity sensor. -4. **Calibration and control algorithms**: interaction (poke) matrix, regularised pseudo-inverse, closed loop. -5. **Hologram / inverse-design algorithms**: weighted GS, MRAF, complex-to-phase-only encoding, quantisation-aware - refinement. -6. **Sensorless optimisers**: SPGD, hill climbing, modal Zernike search (N + 1 measurements per mode). -7. **A fast path**: a monochromatic (cw) spatial run for the AO loop, with pulses used to validate at the end. - -### B3. Proposed split: core primitives, plugin devices and algorithms - -You asked for the physics in the core and the plugin to supply components and algorithms. This is the split I would make. -It follows how the engine already works (every element is declared in `Schema/BuiltIn*.cs` or by a plugin, and every -physical ingredient is a replaceable *model*). - -**Core (physics that is not specific to one device)** - -| primitive | what | why in the core | -|---|---|---| -| `modulator` element | a cartesian grid of cells (pitch, count, offset, rotation), each with a complex transmission or an OPD / phase, given as an array; options: `fillFactor`, `crosstalkKernel`, `zeroOrderLeak`, `phaseWrapRad`, `quantisationLevels`, and `chromatic`: `"opd"` (non-dispersive, a mirror), `"phaseAtDesign"` (phase fixed at a design wavelength and scaled ∝ ω with optional wrapping, an LC cell), `"index"` (Δn(x, y) times a thickness with the material's dispersion) | any plugin or user needs it; a mirror and an LC cell differ only in the chromatic rule, and the rule decides what happens to a femtosecond pulse | -| sub-pixel resampling | average the map over each simulation cell (the `mask` code already does this for edges) | physical pixel edges and gaps; avoids aliasing | -| in-memory map input | arrays through the Python API / C API / run spec without a file; a `map` callback `f(state) -> array` | the AO loop calls this thousands of times | -| coherent source array | an input beam made of N sub-beams: positions, amplitudes, phases, delays, per-beam waist | the fibre bundle and tiled-aperture combining, and any laser array | -| monochromatic spatial fast path | document and test `cw` source + spatial model as the AO inner loop; cache the compiled system between trials (the study machinery already caches prefixes) | AO is spatial first; running 100 frequencies per trial would make the loop unusable | -| zonal wavefront output | `wavefrontSensor` already gives a map; add the slope maps and a stable array output | the control matrix is built from these | - -**`ao` plugin (devices and algorithms)** - -| item | what | -|---|---| -| `deformableMirror` | **fully parametric** (no vendor data needed): actuator layout (square, hex, ring, or an explicit list of positions), count and pitch, clear aperture, **influence function** (Gaussian with a coupling coefficient, or a measured map), stroke limit and optional hysteresis / creep, quantisation of the drive (bits), surface-to-wavefront factor 2 cos(aoi), print-through, optional segmented piston-tip-tilt kind and a bimorph kind. Compiles to a `modulator` with `chromatic: "opd"`. | -| `slm` | **fully parametric**: pixel count (up to 1920 × 1080 and beyond), pitch, active-area size, fill factor, grey levels, the **voltage-to-retardance curve** (tabulated or tanh-like), liquid-crystal thickness, birefringence Δn(λ) (so phase = 2π Δn(λ) d / λ and wrapping at the design wavelength), fringing-field cross-talk kernel, zero-order leakage. Input as a phase target, a Δn(x, y) map, or grey levels / voltages. Compiles to a `modulator` with `chromatic: "phaseAtDesign"` or `"index"`. A 1-D variant for a Fourier-plane pulse shaper. | -| `tiledArray` | a **coherent tiled-aperture amplifier array**: N channels on a square, hexagonal or ring layout (pitch, count, fill factor, tile shape: Gaussian, flat-top or truncated). Per channel: amplitude (power imbalance), **piston phase** (the actuator), tip-tilt, delay and dispersion difference, and optionally its own nonlinear phase (B-integral) or a nested channel system. Output: the coherent sum of the tiles in the near field, propagated to the far field or a focus. Reports the **combining efficiency** (power in the central lobe / total), the far-field Strehl and the grating-lobe fraction. Phase-locking algorithms (SPGD, LOCSET, hill climbing) come from `ao optimise`. Multicore fibre with coupled cores and imaging bundles are **out of scope** for now. | -| `shackHartmann` model | a physical sensor for the `wavefrontSensor`: lenslet pitch and focal length, spot centroiding (CoG, thresholded), photon and read noise, dynamic range, and a reconstructor (modal Zernike, zonal Southwell). | -| `ao calibrate` | poke each actuator (or each SLM pixel group), record the sensor, build the interaction matrix; for a *linear* phase device this is exact, so one run per actuator. | -| `ao correct` | one-shot: command = −G · measurement with G a Tikhonov or truncated-SVD pseudo-inverse; reports residual rms, Strehl before / after, actuators near their stroke limit, null-space and waffle modes. | -| `ao loop` | closed-loop integrator with gain and delay; for the case of a time-varying aberration. | -| `ao hologram` | GS, weighted GS, MRAF, double-phase and Bolduc-type complex encoding, with the SLM's quantisation and wrapping applied in the loop. Output: grey levels and voltages. | -| `ao optimise` | sensorless: SPGD, hill climbing, modal search with a metric you choose (peak intensity, Strehl, encircled energy, SHG signal from a `conversion` instrument). | -| `ao transmissionMatrix` | measure the linear map input-modes → output-modes of a scattering or multimode element by phase stepping; invert for focusing. | - -The plugin declares `"requires": ["fitting"]` if it reuses the estimators (decision 7 of [plan-next.md](plan-next.md)). - -**Why not all in the core?** Device catalogues, vendor-specific curves and control algorithms change fastest and are not -needed by most users. **Why not all in the plugin?** The chromatic rule, sub-pixel averaging, array sources and the fast -path touch the compiler and the propagation, and any user (or another plugin) benefits. - -### B4. The physics that has to be right (and its reference value) - -Every item needs an analytic or published reference in a test (project rule). - -| topic | reference value | -|---|---| -| tilt / piston / defocus from a DM | a pure tilt moves the focus by f·θ; defocus gives the thin-lens focal shift | -| Strehl from rms wavefront error | Maréchal, S = exp(−σ²) (σ in rad), for σ < ~0.3 waves; the engine already reports the estimate, so compare it with the *simulated* focal peak | -| influence function and fitting error | Gaussian influence with coupling c: stroke at a neighbour = c; residual of a Zernike mode fitted by N actuators falls with N, and with c | -| Shack-Hartmann | spot displacement = f · ∂W/∂x; Zernike fit recovers a known mode exactly in the noiseless case | -| SLM quantisation | a blazed grating with N levels has first-order efficiency [sin(π/N) / (π/N)]² (N = 8: 95 %; N = 256: 99.99 %) | -| SLM pixelation | the replica orders are separated by λ / pitch; the envelope is sinc²(fill · m · …) | -| SLM and a pulse | wrapping at 2π at the design wavelength is exact only there; for bandwidth Δω the residual is a spatially dependent spectral phase. A test: a linear ramp (a blazed grating) angularly disperses a pulse as a grating of the same period | -| fibre array | N equal coherent beams give peak intensity N² × a single beam (N × the power in the combined peak); phase error σ costs exp(−σ²); the fill factor sets the side-lobe energy of the far field | -| GS | the error decreases monotonically (Fienup 1982); a known phase is recovered up to the stated ambiguities | -| transmission-matrix focusing | with N controlled phase-only modes, the focus enhancement is (π/4)(N − 1) + 1 on average (Vellekoop & Mosk, Opt. Lett. 32, 2309 (2007)) | -| core coupling | two identical coupled cores exchange power as cos²(κz) | - -### B5. Examples to add (with plots) - -| # | example | shows | key plot | -|---|---|---|---| -| AO-1 | **Component gallery**: a DM (with an influence-function map and a poke), an SLM (grey levels → phase → far field, with 2π wrapping and 8-bit steps), a 19-core bundle (near field, far field) | the three components, as you asked | 3 × 3 panel: device map, wavefront, focus | -| AO-2 | **DM correction of a Zernike-aberrated beam**: calibrate, correct in one shot, show the focus and Strehl before / after, the residual per actuator and the stroke used | one-shot algorithm | before / after focus; Strehl versus number of actuators | -| AO-3 | **Sensorless correction**: SPGD and modal search from a metric only, convergence curves, compared with the one-shot result | iterative algorithm | metric versus iteration, three methods | -| AO-4 | **Aberrated PW-class beam with a DM before the focus and a chirped, nonlinear pulse**: the aberration raises local intensity and the B-integral; the corrected beam does not | what the engine adds beyond other AO tools | peak-fluence map and B-integral map, with and without correction | -| AO-5 | **Beam shaping with an SLM**: GS / WGS for a target intensity in the focal plane. **Smiley face**, **square**, **ring**, **a donut (vortex)**, with the SLM's 8-bit quantisation applied | the requested shaping | target, hologram, simulated result, error | -| AO-6 | **Complex shaping with a phase-only device** (double-phase encoding): amplitude and phase target at once | an important limit of phase-only devices | amplitude / phase error versus method | -| AO-7 | **Coherent tiled-aperture combining**: random phases, then a phase-locking loop (SPGD); far-field peak N², and the effect of a delay mismatch for a 30 fs pulse | tiled-aperture laser array (hex layout, 7 to 61 channels) | far-field pattern before / after, peak versus iteration, and versus delay | -| AO-8 | **Temporal shaping** (see B6): flat-top, double pulse and pulse train from a programmed phase; pixel-gap side-pulses; a Dazzler-like device restoring a gain-narrowed pulse; closed-loop shaping to a target with a simulated FROG | temporal shaping, SLM 4f line and AOPDF | pulse in time for each shape, with the spectrum and phase beside it | -| AO-9 | **Transmission matrix through a scatterer** and focusing | the (π/4)N enhancement | enhancement versus N, with the formula | - -### B6. Transverse and temporal shaping (decision 1) - -**Transverse** shaping (a smiley face, a square, a donut in the focal plane; wavefront correction) is the main target and -is what B1-B5 describe. - -**Temporal shaping**, a comment, since you said you know it less well: - -* *What it is.* A pulse of bandwidth Δω is shaped by changing its spectral phase and amplitude: spectral phase φ(ω) gives - the time profile (a linear φ delays, a quadratic φ chirps, a sinusoidal φ makes a pulse train, a π step makes a - double pulse); spectral amplitude A(ω) sets the envelope. The engine already does this ideally: the `transform` - `spectralPhase` / `spectralAmplitude` operators, the `custom` filter and `chirpedMirror`. -* *How it is done in a lab.* Three standard devices. (1) A **4f line**: a grating splits the colours, a lens maps colour to - position, a **1-D SLM** in the Fourier plane sets phase (and amplitude) per colour, a mirror image of the optics puts the - beam back together (Weiner's pulse shaper). (2) An **acousto-optic programmable dispersive filter** (AOPDF; the Dazzler - is the common product): an acoustic wave in a crystal sets a programmable spectral phase and amplitude. It is the standard - tool in high-intensity chirped-pulse amplifiers, to pre-compensate gain narrowing and high-order dispersion. (3) A **DM or - SLM in the Fourier plane of a stretcher**, which shapes the spectral phase of a stretched pulse. -* *Why a real device is not the ideal transform.* The pixelated Fourier plane has finite spectral resolution (the focal - spot is not a point), pixel gaps and fringing fields, a phase that wraps at 2π and is exact only at one wavelength, and any - misalignment leaves space-time coupling (spatial chirp). These produce pre- and post-pulses at multiples of - 1/(pixel bandwidth), reduced efficiency, and a pulse that is not the one you programmed. The AOPDF has its own limits - (diffraction efficiency, a finite temporal window, a small amplitude ripple). -* *Why it matters to this project.* In a high-intensity laser the shaper sits in front of a gain chain and the nonlinear - elements the engine already models, so the real question is "what does the pixelation of my shaper do to the - contrast and to the compressed pulse at the target?". That needs the device model, the nonlinear chain and the - instruments in one run. This is where the engine adds value over a stand-alone shaper calculator. -* *Staged plan.* (a) **Stage T1, cheap and valuable**: a 1-D `spectralShaper` element: pixels mapped to wavelength through - the dispersion of the grating and the focal length, a Gaussian resolution kernel from the spot size, fill factor, - grey levels, wrapping, optional gaps. It acts on the spectrum directly (no spatial grid), so it is fast and testable - against Weiner's analytic pre-pulse spacing. (b) **Stage T2**: an `aopdf` element (programmable spectral phase and - amplitude with a finite window, efficiency and ripple), and the closed-loop shaping of a pulse with FROG or d-scan in the - loop: the algorithms in B3 apply, with a pulse metric instead of a focal-spot metric. (c) **Stage T3, later**: the full 4f - line with the spatial model (spatial chirp and space-time coupling), which needs the spatio-temporal "profile" mode. -* *Examples.* Flat-top, double pulse and pulse train from a programmed phase; the pixelation artefacts and how the grey-level - and gap settings change them; **a pulse shaped by a Dazzler-like device before a stretcher and compressor so that the - gain-narrowed output is restored**; closed-loop shaping to a target pulse with a simulated FROG. -* *Usefulness.* High in the ultra-high-intensity community (spectral phase control and gain-narrowing compensation are daily - tasks), and the same loop algorithms serve both domains, which is a good reason to build them once. - -### B7. Usefulness to the community - -An honest assessment, split by device: - -| item | usefulness | reasoning | -|---|---|---| -| DM + wavefront sensing + correction in a *pulse and nonlinear* model | **High** for the ultra-high-intensity community | Focal-spot quality sets the peak intensity at every PW-class facility, and DMs are standard there. What is rare is a tool that puts the wavefront in the same model as chirp, B-integral and nonlinear cleaning, so one can ask "what does this aberration do to the B-integral and to the damage margin of the next optic?" | -| Coherent tiled-aperture combining | **High, topical** | Tiled-aperture coherent combining (the ICAN / XCAN line of work) is a main route to high repetition rate at high energy. A model that includes the **pulse** (delay mismatch, dispersion per channel, B-integral per channel) is less common than a pure Fourier-optics one. | -| SLM beam shaping | **Medium** | Widely used (cold atoms, microscopy, laser machining), and well served by existing tools. For ultrafast lasers its value is the *chromatic* behaviour of wrapped phase, which many tools ignore. | -| Temporal pulse shaping (SLM 4f, AOPDF) | **High for high intensity, high for spectroscopy** | Standard in the ultrafast community, and the Dazzler-type device is in nearly every high-intensity chirped-pulse amplifier; pixelation artefacts are a classic source of error. See B6. | -| Sensorless optimisation and transmission matrices | **Medium** | Useful to teach and to test control code before it touches a real mirror. | - -Caveats that should be stated in the docs: other open packages already cover single-frequency AO well (to my knowledge, -HCIPy and aotools for astronomy-style AO; LightPipes for diffraction). The engine should not claim to beat them at that. Its -value is the combination: pulse + nonlinearity + chromatic devices + the digital twin below. Models of actuators are -idealised unless the user supplies measured influence functions; the docs must say so. - -### B8. Risks and open points - -* **Scale is a requirement (decision 5): a full 1920 × 1080 SLM.** One complex double-precision slice is 33 MB at one cell - per pixel (1920 × 1080), 133 MB at 2 × 2 cells per pixel (3840 × 2160, enough to show fill factor and cross-talk), and - 268 MB padded to 4096 × 4096 for the FFT. The engine carries the profile at every stored frequency, so 100 frequencies - would need about 27 GB at the padded size. Three measures, each to be confirmed by the stage-0 measurement before it is - built: (1) **stream over frequency**: for a chain of *linear* spatial elements the frequencies are independent, so run - one frequency at a time and keep only what the instruments need (memory = one slice); (2) **sparse spectral sampling**: - a mask, SLM or DM changes smoothly with ω (apart from diffraction angles ∝ λ), so use K ≈ 8-16 frequencies across the band and - interpolate, with a built-in error estimate against a dense run; (3) the **GPU spatial back-end** (CUDA / OpenCL, where - available) and a cw / narrow-band mode for the AO loop. Apple silicon runs the CPU path (Accelerate FFT); the - full-size SLM examples must therefore fit a laptop CPU in minutes, and the roadmap carries an explicit time and memory - budget for them. -* **Chromatic SLM physics** is the part most likely to be wrong in a first version. Start with a monochromatic test and - add the pulse case against the blazed-grating-as-a-grating reference. -* **Vendor numbers** (stroke, hysteresis, voltage curves) are not in the repository. Ship generic presets labelled as such - and a loader for measured data (the pattern used for detector presets). -* **Apple silicon**: nothing here needs CUDA, but a large spatial run is faster on the GPU plugin, which cannot use Apple - GPUs (no FP64). Keep the AO examples small enough for the CPU. -* Zernike-only wavefront correction cannot represent a DM's mid-spatial-frequency errors; the zonal path matters. - -### B9. Staging and sizes - -The work packages with ownership, tests, example deliverables and acceptance criteria are in -[roadmap-ao-twin-2026-11.md](roadmap-ao-twin-2026-11.md). Summary: - -| stage | content | size | -|---|---|---| -| 0 | measurements and spikes (scale, speed, chromatic SLM, SPGD with CMA-ES, tiled array in a Branched system) that decide the primitives | S-M | -| 1 | core: `modulator` (chromatic rules, sub-pixel averaging, in-memory map), streaming and sparse spectral sampling for linear spatial chains, cw fast path, coherent tile source, tests | L-XL | -| 2 | `ao` plugin: `deformableMirror`, `slm`, `tiledArray`, plugin docs, device tests | L | -| 3 | sensing and control: `shackHartmann` model, Zernike modal and zonal reconstructors, `ao calibrate`, `ao correct`, `ao loop` | M | -| 4 | holograms (GS family, complex encoding) and sensorless optimisers (SPGD, modal, phase locking) | M | -| 5 | temporal shaping: `spectralShaper` (T1), `aopdf` and closed-loop pulse shaping (T2) | M | -| 6 | examples and visual deliverables (plots and short animations for every iterative algorithm) | M-L | - -Total roughly 6-8 weeks of focused work for the AO plugin with the temporal part; the digital twin (part C) is a separate, -partly parallel stream. - ---- - -## C. Digital twin and a backend for AI-assisted laser design - -### C1. What already supports it (*checked* unless stated) - -| piece | what exists | -|---|---| -| model of the known system | every element in [capabilities](../capabilities.md), plus ghosts, delayed light, measured input spectra, pedestal from a measured contrast trace | -| "unknown" blocks | fitting-plugin `transform` elements with physical meaning: spectral phase (GDD, TOD, FOD, spline knots with a smoothness penalty), spectral amplitude, energy, Jones matrices, replicas, phase noise, ASE pedestal, Zernike wavefront | -| lab data | the core's `measured` data type: any instrument compared with a recording on its own axis, at its resolution (spectra, FROG, d-scan, contrast, cameras, polarimeter, energy) | -| fitting | Levenberg-Marquardt, CMA-ES, MCMC, priors, instrument-response parameters; three result layers (simulated, inferred, believed); influence map (what the data can and cannot constrain); provenance; a fitted system file for new inputs | -| backward propagation | a measured field is propagated back through the fitted system to the input (example 26) | -| agents | `lab-mirror` agent and `mirror-lab-setup` skill (build, calibrate and check a model of a lab), `setup-optimiser`, `debug-setup`, `AGENTS.md`, `llms.txt`, JSON schemas, strict option checking, typed Python builders | - -### C2. What is missing or not checked - -| gap | detail | size | -|---|---|---| -| **One clear worked example** | The pieces exist, but there is no single end-to-end story: "build the twin, place transforms, collect data at several settings, fit, validate on a setting you did not fit, then explore". This is the main deliverable. | M | -| **Joint fit over many operating conditions** (*not checked*) | Is a single transform set fitted to several recordings, each with its own settings (pump energy, gas pressure, grating separation) in one cost function? Example 26 shows a single fit. If it is not supported: a `conditions` list in the `fit` block, each with parameter overrides and its own measured data. | M | -| **Held-out validation** | A convention and a helper: fit on some conditions, report the error on the others. Without it a twin only reproduces its training data. | S | -| **Where to put transforms** | A "twin planner": from the layout and the instruments available, list the places where a transform is identifiable (the influence map answers it after the fact; a planner would answer it before). | M | -| **Which data to collect next** | Optimal experimental design: rank candidate operating conditions by how much they reduce the posterior width (the MCMC posterior is already there). | M | -| **Black-box (neural-network) transforms** | A transform whose parameters are network weights. Two points of honesty: (1) the repository rule is *no NuGet packages*, so a network would be a small hand-written MLP in the engine or a Python callback (user elements and user responses already take Python callbacks); (2) training through nonlinear propagation needs gradients, and the engine has none. Options below. | see below | -| **Drift in time** | Laser state changes over hours. Roadmap already lists ensemble Kalman and Bayesian estimators for this. | L | -| **Uncertainty on predictions** | MCMC samples exist; a ready function "predict instrument X at new settings with a credible band" does not (*not checked*). | S to M | -| **Wavefront twin** | Today the `wavefront` transform fits Zernike terms. With stage 1-3 of B it could fit a DM / SLM *map* or a measured wavefront, so the twin includes the beam quality. | M, after B | - -**Neural-network transforms, options:** (a) **do not use a network first**: spline-knot transforms with a smoothness prior -are a flexible black box with a physical meaning (a spectral phase is a function of ω), already supported, and are -identifiable with a few tens of parameters; (b) a Python-callback network trained with CMA-ES for up to ~100 weights, no -engine change; (c) an adjoint (backward) gradient through the split-step solver, giving gradients for thousands of -weights: XL and a real research item; (d) export a surrogate of the engine to a differentiable framework: separate project. -Recommendation: build the example with (a) and (b), say plainly that (c) is a possible later step, and describe where a -network is *not* needed. - -### C2b. Where it lives (decision 6): fitting gets conditioned transforms, a new `twin` plugin sits on top - -**Fitting plugin: transforms that know the operating condition (F1-F4).** Today a `transform` is one fixed operator. Real -unknowns change with the condition: a spectral phase that grows with pulse energy (nonlinear phase), a thermal lens -that grows with average power, a gain narrowing that depends on pump. The proposal is a *conditioned transform*: the -operator depends on a condition vector θ (pulse energy, pressure, pump current, grating separation, temperature, ...), -with **parameters shared across all conditions**, and a fit that uses **all recordings at once** (the `conditions` list: for -each, the settings that override the system file and the measured data). - -| item | what | notes | -|---|---|---| -| F1 | **Multi-condition fit**: a `conditions` list in the `fit` block; one cost function over all recordings; per-condition nuisance parameters (a pointing offset, a detector gain) kept separate from the shared ones; **held-out conditions** for validation | needed by everything below. *Not checked* whether any of this exists; the docs show a single dataset. | -| F2 | **Parametric conditioning**: each transform parameter is a low-order function of θ (constant, linear, power law, polynomial), with physical priors (for example the nonlinear phase is linear in energy) | the most interpretable and the best-conditioned; ship first | -| F3 | **Smooth bases in (ω, θ)**: tensor-product splines or Chebyshev for spectral phase and log-amplitude, with smoothness penalties in both directions, plus the passivity, causality and Kramers-Kronig constraints the plugin already has | the "black box with a physical meaning" | -| F4 | **Network transforms** (`kind: "network"`): a small multilayer perceptron g(ω, θ; w) giving spectral phase and log-amplitude, with the same hard constraints (|T| ≤ 1, causality, smoothness) built into the output layer, trained with the fitting plugin's estimators | see the training note below | - -**On "PINN".** A physics-informed neural network, strictly, puts the residual of a governing equation in the loss. Here the -forward model is the engine itself, so the closer description is a **grey-box (hybrid) model**: the known physics is the -engine, the unknown part is a constrained network inside it. The constraints (energy conservation, passivity, causality, -smoothness, and optionally a Kramers-Kronig relation between amplitude and phase) are the "physics-informed" part. I would -describe F4 as "physics-constrained network transforms" in the docs, and keep the word PINN for a possible later -approach that actually uses an equation residual. - -**Training note (the honest limit).** The engine has no automatic differentiation. The fitting plugin's estimators are -Levenberg-Marquardt (a finite-difference Jacobian costs one run per parameter per condition), CMA-ES and MCMC. That is -fine for tens to a few hundred parameters (F2, F3, a small F4 network) and not for thousands of weights. A real path to -gradients exists for a transform followed only by *linear* elements, where the chain rule is exact and cheap through -backward propagation; the nonlinear segments need an adjoint of the split-step solver (a research item, XL). The roadmap -treats gradients as a stage-gated option, not a promise. - -**`twin` plugin (requires `fitting`).** The project-level workflow that sits on top; it adds no physics. - -| item | what | -|---|---| -| T1 | a **twin project** layout: nominal system, where the transforms are, a catalogue of recordings by condition, fit and validation splits, provenance, a one-command rebuild | -| T2 | **placement planner**: from the layout and the available instruments, which places are identifiable (a prior to the influence map) | -| T3 | **experiment design**: rank candidate operating conditions by the reduction of the posterior width, so beam time is spent where it informs most | -| T4 | **validation report**: prediction error on held-out conditions with credible bands; a model that only reproduces its training data is flagged | -| T5 | **what-if explorer**: scan or optimise on the fitted twin (the `setup-optimiser` route), with the believed-layer uncertainty on every prediction | -| T6 | **drift tracking**: re-fit a few parameters as new recordings arrive (ensemble Kalman, roadmap item) | -| T7 | the **MCP tool server** (C6), because it is the twin that an agent should talk to | -| T8 | an HTML **twin report** (fit quality, parameters with uncertainty, held-out validation, plots) for the lab book | - -### C3. The AI-assisted design backend - -What makes the engine a good backend for an AI agent: structured JSON in and out; a schema; strict mode that turns unknown -keys into errors; warnings that name problems (grid too small, SPM at zero frequency); a loss budget that closes; damage -margins; scans and optimisers; an `AGENTS.md` and `llms.txt` written for agents. - -What would make it better (*not checked* means I did not find it): - -| gap | detail | size | -|---|---|---| -| **A tool server for agents** | An MCP server. Trade-offs and a recommendation in C6. | M | -| **A design-goal format** | One file saying "compress to < 30 fs, B < 1, contrast > 10¹⁰ at −10 ps, fluence below the damage threshold", checked automatically, returned as pass / fail per goal. The engine already computes each quantity. | M | -| **Run budget and caching** | An agent may call the engine hundreds of times; cached prefixes exist for studies. Document the cost per run for the example systems. | S | -| **A benchmark of design tasks with known answers** | Gives a way to measure whether an agent design is good, and to catch hallucinated physics. | M | -| **Honest scope statement** | The engine is not a cavity or ray-trace designer (see Limitations). An agent must be told this. | S | - -### C4. Examples to add - -| # | example | shows | -|---|---|---| -| DT-1 | **"Build the twin"**: a synthetic lab (a known system with hidden aberrations, so the answer is known; the repository already uses synthetic data with answers in `docs/assets`). The twin starts from the nominal parameters, transform blocks are placed at three places, data from five operating conditions are fitted, the hidden values are recovered and a sixth condition is predicted with an error band. | -| DT-2 | **Using the twin without beam time**: scan the compressor, the pump energy and the gas pressure on the fitted model and read the predicted FROG, spectrum and contrast. | -| DT-3 | **Spline-knot and network transforms side by side** on the same data, with the identifiability map: when does the black box help, and when does it only fit noise. | -| DT-4 | **Agent-driven design**: an AI agent (through the tool server) designs a compressor for a stated goal, with the engine's warnings visible, and the result checked against the goal file. | -| DT-5 | **The wavefront twin** (after B): fit a DM / SLM map from a measured wavefront and a focal spot. | - -### C5. Usefulness to the community - -* **Digital twin: high.** Time on a high-intensity laser is scarce and expensive, and nearly every facility has partial - models and many diagnostics. The distinctive part is the honest treatment: three result layers (simulated, inferred, - believed), an influence map that says what the data cannot constrain, and held-out validation. This is not a feature - most simulation codes have, and it uses what is already built. It also connects every other piece of the engine - (instruments, measured data, nonlinear cleaning, the AO plugin). -* **AI-assisted design: medium to high, but riskier.** The value depends on the physics being right and on the agent being - told where the engine's scope ends. The benchmark and the goal file are what make this credible rather than a demo. -* **Neural-network transforms: medium.** Interesting, but a spline or physical-parameter transform is easier to trust. - Do not lead with it. - -### C6. An MCP server: trade-offs - -MCP (Model Context Protocol) lets an AI client call typed tools exposed by a program. Today an agent such as Claude Code -already drives the engine through the CLI with `AGENTS.md`, `llms.txt` and the skills, so the question is what a server adds. - -| | with an MCP server | CLI + skills (today) | -|---|---|---| -| **Reach** | works in any MCP client (desktop chat apps, IDEs, other agents), including clients with no shell | needs a client that can run shell commands | -| **Structure** | typed inputs and outputs; the model cannot mistype a flag; results can be compact summaries instead of file dumps | JSON files and text; the agent must know the conventions (the docs and `AGENTS.md` teach them) | -| **State and speed** | a long-lived process can keep the engine loaded and a compiled system hot: faster loops of many runs, progress for long jobs | each call starts a process; fine for single runs, slower for hundreds | -| **Context cost** | every tool description is in the model's context on every turn; many tools cost real tokens (keep it to ~8 coarse tools, or load them on demand) | zero until the agent reads a doc or skill | -| **Safety** | can be scoped: a workspace folder, read-only tools versus run tools, a run-time budget, no network | the agent has the whole shell; safety is the client's permission system | -| **Danger** | engine inputs can contain **user expressions and Python callbacks**, which is code execution; a server must refuse them by default or run in a sandbox. Connecting a twin to real lab hardware would be a safety issue: keep the server **read-only with respect to the lab** | the same code-execution risk exists, but it is the user's own session | -| **Maintenance** | one more component to version in step with the engine, test on three operating systems and document | none | -| **Debugging** | an extra layer between the model and an error | the agent sees raw stderr | - -*Recommendation.* Build it, but small and optional: a Python server (stdio) in the `twin` plugin, installed with an extra -(`pip install "ultrafast[mcp]"`), exposing about eight coarse tools: -`describe` (capabilities and limits, the content of `llms.txt`), `validate`, `run` (returns a compact summary plus -artefact paths), `compare_to_measured`, `scan`, `fit`, `explain_warning`, `plot` (returns an image). Resources are the -reference pages and the schemas. Defaults: workspace-scoped, user expressions and callbacks refused unless enabled, a -time and memory budget per call. Build it after the twin basics (T1-T5), because an agent is only as useful as the -model it talks to. If few users need it, the CLI route remains. - ---- - -## Suggested order - -See [roadmap-ao-twin-2026-11.md](roadmap-ao-twin-2026-11.md) for the full, ordered work packages. In one line: CI fixes, -then measurements and spikes, then the core primitives, then the AO and fitting streams in parallel, then the twin plugin and -the MCP server, then examples and visuals throughout. - -Examples AO-1 … AO-9 and DT-1 … DT-5 are scheduled, with plots and animations, as E1-E15 in the roadmap. - -## Open questions - -1. **Spectral sampling for the full-size SLM** (B8): is a sparse set of 8-16 frequencies acceptable for your pulses - (bandwidth and the aberrations you care about)? The stage-0 measurement will give an error estimate to decide. -2. **Condition variables** (F1): which operating conditions do you actually vary in the lab (pulse energy, gas pressure, - compressor separation, pump current)? They set the first conditioned-transform examples. -3. **Tile model** (`tiledArray`): is a per-channel *amplitude, phase, delay* model enough for the first version, with a - nested per-channel amplifier system as a second step? -4. **MCP**: build it as the last item of the twin stream, as recommended, or earlier? diff --git a/docs/design/plan-fixes-2026-11.md b/docs/design/plan-fixes-2026-11.md deleted file mode 100644 index 805ee94..0000000 --- a/docs/design/plan-fixes-2026-11.md +++ /dev/null @@ -1,108 +0,0 @@ -# Plan: fix the engine bugs and gaps found in the November 2026 consolidation - -**Status: complete (2026-11-08, released as 2.2.0).** What remains open is in the roadmap and in `HANDOFF.md`. - -Findings and numbers were collected in a progress note that was removed when this plan was completed (2026-11-08); what remains open is in `docs/roadmap.md` ("Open items found by the November 2026 consolidation"). -Four implementers work in separate git worktrees on disjoint code; the lead merges, regenerates the generated files, re-runs the -component tests and paper replications, and writes each paper's "how well the science matched" section. - -| stream | items | main code | tests / checks to flip | -|---|---|---|---| -| 1 grid and solver | B1 2 µm / 10 fs SPM, B2 angular-dispersion carrier snap, B3 astigmatic round aperture, B4 binaryPhase, B9 stepper on phase-mismatched crystals, B10 delayed-response memory in the grid planner, B13 fibre selfSteepening, B14 D(λ) interpolation | `Engine/Compiler*`, `GridPlanner`, `Optics/` gratings, prisms, apertures, masks, `Physics/Nonlinear`, `Crystals/` | `media.py`, `optics_misc.py` KNOWN marks | -| 2 instruments and retrieval | B5 energy-meter band edges, B6 IAC peak, B7 cross-correlator `resolutionS`, B12 retrieve kind, G5 SD-FROG / XFROG / multi-trace retrieval, G6 gate-arm element | `Instruments/`, `Retrieval/` | `instruments_more.py` | -| 3 materials and crystals | B8 KDP cut deff, B11 anisotropicFraction on expression responses and > 1, G1 cubic χ(3) tensor + BaF2 (XPW), G2 CS2 material | `Crystals/`, `Materials/`, `Physics/Nonlinear` | `crystals_chi2.py`, new tests | -| 4 beams and geometry | G3 Kerr-matched cell mode helper, G4 single-shot time-to-space cross-correlator | `Beams/`, `Instruments/`, `Systems/` | `spatial.py`, new tests | -| lead | B15 docs generator links, re-verify `externalAngleDeg`, merge, `/regen`, paper re-runs, docs | | | - -Rules for implementers: a test with an analytic or published reference per fix (physics accuracy first), header comments on new -files, reference docs for new options, regenerate nothing (the lead does), no edits to CHANGELOG / testing.md / PROJECT_STATUS (report -the lines instead), no push, commit in the worktree branch. - -Deferred (recorded in the roadmap): air scatter and stripe filter of a single-shot correlator, crystal-face reflection spikes, -a multi-order Fabry-Perot of finite beams. - -## Status (2026-11-08) -Four implementers run in git worktrees under `.claude/worktrees/` (branches `worktree-agent-`): -stream 1 `a29b2025f815cde80`, stream 2 `a3949310445871089`, stream 3 `a210869c846351b77`, stream 4 `a4524c7a8bd6287c1`. -They were stopped twice by the usage limit and resumed with SendMessage; their work is saved in their worktrees. To continue after -another interruption: `git worktree list`, `git log` in each, SendMessage "continue" to the agent id. -Lead checks: `externalAngleDeg` works (declared on `interaction`, tested in NoncollinearOpaTests; the earlier "unknown parameter" was a -stale `bin/native`). Open: the phase-matching calculator rejects `noncollinearDeg` for `sfg` although the schema allows it. -After the streams report: merge the four branches into `dev/2026-11-consolidate`, `/regen`, fix the generated "More:" links (B15), -re-run `tests/components/run_all.py`, `build/paper_check.py` (set ULTRAFAST_LIBRARY first), add "how well the science matched" to each -paper README, then update capabilities, roadmap, PROJECT_STATUS, CHANGELOG, testing.md (done). - -## Collected after the streams (changelog lines, paper follow-ups, testing.md rows) - -### Stream 2 (merged): instruments and retrieval -* CHANGELOG Fixed: `energyMeter` and camera `bandM` weight edge bins by the fraction inside the band; interferometric autocorrelator delay - axis symmetric with d = 0 and analytic background (peak:background 8 for all carriers; note when `points` under-samples fringes); - `crossCorrelator` with `resolutionS` reports trace, floor and `contrastAt_*_db` relative to the resolution-limited peak; `retrieve` no - longer silently assumes SHG-FROG (kind from `kind`, from the data, or the instrument's `frogType`; asymmetric trace without kind = error). -* CHANGELOG Added: `frog` `type: "sd"`; `xfrog` (alias `sfgFrog`) with a known `reference`; retrieval kinds `sd-frog`, `xfrog` for - `pcgpa` and `epie`; multi-trace `multiFrog` (double FROG) and Python `uf.retrieve_multi`; `crossCorrelator` `gateArm`. -* testing.md rows: InstrumentFixesTests (band edges vs Gaussian CDF, IAC 8 for five pulses, cross-correlator resolution consistency, gate-arm - ghost levels R^(2k)); FrogKindsTests (PG / SD centroid slopes, Kane & Trebino App. C; XFROG slope and sum frequency; retrieval with sign; - kind inference); MultiTraceRetrievalTests (double FROG, six starts, Kida et al. 2014). -* Paper follow-ups: ma-2018: delete `repro_1.py`, make the two `res1ps_*` checks hard, use `gateArm` instead of the Branched workaround. - kida-2014: use `xfrog` and `retrieve_multi` instead of numpy; drop the "no XFROG / multi-trace" caveat. kane-1993: SD gate + XFROG now exist. -* Not done: CLI `retrieve` stays single-trace; XFROG with a measured reference from a file; a probe-arm-only element option. - -### Stream 3 (merged): materials and crystals -* CHANGELOG Fixed: KDP type-I d_eff with an explicit cut changed erratically with φ (ordinary-mode eigenvector came from rounding noise); - `anisotropicFraction` honoured by `expression` and `callback` delayed responses, range 0..1.5 (1.5 = purely orientational), explicit key - on `linAgrawal` replaces its own split. Behaviour change: values > 1.5 or < 0 are errors. -* CHANGELOG Added: built-in `CS2` (Kedenburg index; n2 3.0e-18 m²/W with 90 % delayed; `orientational` delayed-response kind) and `BaF2` - (Li 1980 Sellmeier; n2 2.0e-20; σ = −1.2); cubic Kerr anisotropy (`kerrAnisotropy`, `kerrCutAxis`, `kerrAzimuthDeg`) with XPW. -* UNVERIFIED literature values: CS2 total n2, 90/10 split, 1.6 / 0.2 ps times; BaF2 n2 and σ (citation from memory). Flagged in docs. -* XPW law from the tensor: [001] cut ∝ sin²(4β) (zero at 0 and 45°, max at 22.5°); [011] cut ∝ sinβ cosβ (sin²β/2 − cos²β), zero at 54.7°. - The Kaur paper's 64.5° angle convention is not tied to this yet. -* Not done: GPU cubic term; spatial and carrier-resolved cubic term (throws); net48 tests, Python tests, mkdocs and examples not run in the - worktree. -* testing.md: MaterialsCrystalsTests (14 tests: KDP d_eff, CS2, BaF2, cubic χ(3)/XPW); NerTests +3. -* Paper follow-ups: liu-2013 and shimizu-1967 use built-in `CS2`; kaur-2024 can model XPW with `BaF2` + `kerrCutAxis`/`kerrAzimuthDeg`; - maker-1962 re-check KDP d_eff vs φ. - -### Stream 4 (merged): cell mode and single-shot correlator -* CHANGELOG Added: `cellMode` command and `ultrafast.cells.cell_mode` (Kerr-matched Gaussian eigenmode of a two-mirror multi-pass cell, - Herriott or quasi-concentric, gas fill or plates; empty-cell Kogelnik-Li mode, re-entrant spacing, Gouy phase, B-integral; second-moment - Kerr lens agrees with the radial model's rms radius to 0.3 %); instrument `singleShotCorrelator` (window and resolution from the crossing - geometry, Ma et al. 2018 eqs. 1-2; trace on position and delay axes; area conservation). -* testing.md: CellModeTests (7), SingleShotCorrelatorTests (8, 1 slow ~150 s), python/tests/test_cells.py (2). -* Not simulated by `singleShotCorrelator`: beam propagation in the crystal (efficiency, depletion, envelope across the overlap, walk-off), air - scatter, stripe filter, crystal-face reflections, fibre array. The spatial `sfg` crystal on a cartesian grid does reproduce the mapping. -* Not done: cellMode is a command / Python function only (no element option), round beams only, peak power only. -* Paper follow-ups: schulte-2016 and kaur-2024 should use `cell_mode` for the launch beam instead of trial; ma-2018 compare `singleShotCorrelator`. - -### Stream 1 (merged): grid and solver -* **User-visible change:** the input pulse is built at the REQUESTED carrier (sub-bin offset on the lattice), not the carrier snapped onto the - lattice; Kerr, self-steepening, angular-dispersion reports and carrier wavelength follow it (`grid.ReportOmega`). Window frame still moves - with the lattice-centre group delay: a dispersive pulse drifts by GDD × offset × L relative to the window (73 fs for a 1 ps soliton at - 515 nm over 3 L_D); pulse intact. Moving the frame to the pulse carrier is a larger change, not done. Coarse default grid (12 points per - FWHM): a 10 fs soliton with off-lattice carrier is 0.3 % off; exact with `snapCarrier: false`. -* CHANGELOG Changed: input pulse built at the requested carrier; grid-dependent fibre SPM phase error (0.1-0.4 %) gone. Fixed: - `trackedAngularDispersion` of gratings and prisms; round aperture on an elliptical beam (exact integral); strongly phase-mismatched thick - χ(2) crystal (≤ 0.5 rad mismatch phase per step); grid planner includes the memory of a delayed response, short fixed windows reported; - element-level `selfSteepening` on fibres and bulk; fibre D(λ)/β2 tables use a cubic spline. Added: warning and `spectrum at non-positive - frequency` loss booking when nonlinear broadening reaches ω ≤ 0. Docs: binaryPhase edge loss is a sampling effect (formula in transverse.md). -* testing.md: GridSolverTests (9 tests), BeamSourceTests +1. -* **B1 profile distortion at 2 µm / 10 fs is NOT fixed, only reported:** the SPM spectrum of a 1.5-cycle pulse at B = 3 reaches ω ≤ 0 (analytic - signal has no negative frequencies). Three media.py checks stay KNOWN. Fix would need a carrier-resolved model. -* Not covered: delayed-response memory only for Raman (not generic `timeResponse` or gas revivals); decentre of round aperture still ignored; GPU - untested; carrier offset in the carrier-resolved model only covered by existing tests. -* Paper follow-up: maker-1962 `repro_2.py` and shimizu-1967 `repro_1.py` now behave as expected: convert to documented regression checks or delete. - -## Status: all four streams merged into dev/2026-11-consolidate (2026-11-08) -* C# suite 467 + schema fix green; component tests 1688 / 1691 (3 KNOWN = B1 2 µm / 10 fs); paper_check 11 of 12 green. -* **Regression to diagnose:** shimizu-1967 hard check "engine (CS2 dispersion off) = lowest-order model" went from 6e-4 to 5.8e-3 (tol 5e-3) - after the merge (requested-carrier pulse or longer auto window?). Kida soft checks 2 -> 4. -* Next: four paper agents re-run the papers with the new features and add "How well the science matched" to every README; - the progress note was deleted and the findings are folded into capabilities / roadmap / CHANGELOG / testing.md / PROJECT_STATUS. - -### Paper re-runs (FROG, Kida, Ma) — open engine question -* Two-trace `multiFrog` (reference SHG trace + XFROG trace only) ends in the wrong-direction basin in 13 of 16 noisy starts and in 6 of 6 - noise-free starts (deep local minimum, not an exact twin: trace error does not drop from 600 to 3000 iterations); with the third trace - all starts find the truth. Whether `multiFrog`'s initial guess / step order should differ is open. Repro: - `docs/papers/kida-2014-double-frog/repro_2.py` (~10 s). -* ePIE SD GDD fit needs the >5 %-of-peak spectrum threshold (wing phase gives a fit artefact); `singleShotCorrelator` places a −60 dB peak at - +20 ps for a pre-pulse at −20 ps when the SH gate carries e²: documented, not a paper statement. diff --git a/docs/design/roadmap-ao-twin-2026-11.md b/docs/design/roadmap-ao-twin-2026-11.md deleted file mode 100644 index 8eb98d8..0000000 --- a/docs/design/roadmap-ao-twin-2026-11.md +++ /dev/null @@ -1,569 +0,0 @@ -# Roadmap: adaptive optics, temporal shaping, conditioned transforms, digital twin, examples - -Written 2026-11-09, revised the same day, on the branch `dev/2026-11`. The reasoning and the physics are in -[plan-ao-digital-twin-2026-11.md](plan-ao-digital-twin-2026-11.md). This page is what you paste into Claude Code: **five -session briefs** (S1-S5), each self-contained, with the model and advisor to use, the files to read and own, the order of -work, the tests and the acceptance criteria. Sizes: S ≈ 1 focused agent-day, M ≈ 2-3, L ≈ 4-6, XL ≥ 7. - -## 1. The session map - -| session | what | depends on | executor / advisor | size | -|---|---|---|---|---| -| **S0** (done, in the planning session) | CI fixes, logo, licence, README, this plan | - | - | done | -| **S1 Foundation** (**done 2026-11-09**) | Python package structure; plotting consolidation, animation, Zernike plot; examples regrouped into folders and an `ultrafast-examples` package; run-all figure scripts; optical-computing examples | S0 | Sonnet 5.5 / Opus 5.5 (1 call) | L | -| **S2 Core and spikes** (**done 2026-11-10**) | measurements that decide the design (scale, sparse spectral sampling, chromatic SLM, repetition), then the core primitives: `modulator`, in-memory maps, streaming and sparse spectral sampling, coherent tiles, fast path | S1 | Opus 5.5 / none | XL | -| **S3 AO plugin** (**done 2026-11-10 on `dev/2026-11-s3-ao`, not merged**) | `ao` plugin: deformable mirror, SLM, tiled array, sensing, calibration and control, holograms, sensorless optimisers, temporal shaping | S2 | Sonnet 5.5 / Opus 5.5 (2 calls) | XL | -| **S4 Fitting and twin** (**done 2026-11-10; T6 MCP server not built**) | conditioned transforms in the fitting plugin; `twin` plugin; MCP server; goal files | S1 (not S2/S3: runs in parallel with them) | Sonnet 5.5 / Opus 5.5 (3 calls) | L-XL | -| **S5 Examples, docs, verification** (**done 2026-11-10 except the items in `notes/s5-release.md` section 3: E1, E6, E13, E14, E15, the older examples' story pass**) | the AO and twin examples with plots and animations, docs, status, the full verification and the merge to `main` | S2, S3, S4 | Sonnet 5.5 / Opus 5.5 (1 call) | L | -| *S6, later, optional* | repetition and switching: Pockels cell, pulse picker, pulse-train mode (see section 6) | S2 | Opus 5.5 / none | L | - -**Order.** S1 first, because it restructures the Python package and moves the examples; any example written before that would -be written twice. After S1, **S2 and S4 can run in parallel** (they own different files). S3 waits for S2. S5 is last. - -``` -S0 ── S1 ──┬── S2 ── S3 ──┐ - └── S4 ────────┴── S5 ── (S6) -``` - -## 2. Which model and which advisor - -**Available to you: Sonnet 5.5 (`claude-sonnet-5-5`) and Opus 5.5 (`claude-opus-5-5`); no Fable, and a limited token budget.** -Opus 5.5 is the strongest model you have, so **nothing can advise it**: where a session needs the strongest model it runs *on* -Opus with no advisor, and the safety net is the reference-value tests and the owner's gate. Everywhere else the cheaper -combination is a **Sonnet executor with an Opus advisor**, called a few times at the decisions that matter. - -*Reasoning, not measurement*: the split follows how much physics judgement each session needs against how much plumbing it -contains. Check `/usage` after S1 and move a session up or down if the numbers surprise you. - -| session | executor | advisor | effort | why | -|---|---|---|---|---| -| S1 | **Sonnet 5.5** | **Opus 5.5**, one call | medium | a wide, mostly mechanical refactor and move; the risk is broken imports and links, not physics. One advisor call on the old → new name table and the example regrouping, before any file moves | -| S2 | **Opus 5.5** | none | medium | the chromatic SLM model and the streaming / sparse-sampling error control are where a wrong physics decision is expensive, so this is the one session that runs on the strongest model. Safety net: the reference tests, gate G0 (the owner reads the spike note), and one `physics-reviewer` pass | -| S3 | **Sonnet 5.5** | **Opus 5.5**, two calls | medium (medium/high if needed for the SLM pulse model) | S2 has settled the physics design; S3 is device models and algorithms with reference values. Advisor (1) before the SLM chromatic model and (2) before "done". If Sonnet cannot make a reference test pass in two attempts, switch the session to Opus for that step only | -| S4 | **Sonnet 5.5** | **Opus 5.5**, three calls | medium/high | plumbing, statistics and a server. Advisor at the multi-condition fit design (F1), the MCP security design (T6) and before "done" | -| S5 | **Sonnet 5.5** | **Opus 5.5**, one call | medium | examples, docs and verification; one call before the merge to `main`. `test-runner` (haiku) runs the long jobs | -| S6 | **Opus 5.5** | none | medium | switch physics (Pockels cell, regen timing) | - -* **An advisor must be at least as capable as the executor.** Opus 5.5 advising Sonnet 5.5 is valid. If your Claude Code lets you - choose the advisor model, choose Opus 5.5; otherwise leave the default and say so in your first message (I could not check how - your setup selects it). Where the table says *none*, do not use an advisor. -* **Advisor calls are the expensive part.** Make them where the table says, no more: once after orientation and before the first - edit (where listed), when a result disagrees with a reference value and two attempts did not fix it, and once before declaring - done with the work already committed. -* **Use `test-runner` (already `haiku`) for builds and test runs.** Do not start `paper-replicator`, `bug-hunter` or - `physics-reviewer` agents unless a brief says so; each starts cold and runs on the session's model. A `physics-reviewer` pass - is required once, at the end of S2 and of S3. -* **Keep context small.** Give a session only its brief, read only the files its brief lists, and follow the token-economy - rules in `CLAUDE.md` (never read generated files). - -## 3. Rules that apply to every session - -* **Branches and worktrees.** Cut a branch from `dev/2026-11` per session: `dev/2026-11-s1-foundation`, `-s2-core`, `-s3-ao`, - `-s4-twin`, `-s5-examples`. Work in a git worktree if two sessions run at once. Merge into `dev/2026-11` when the - acceptance criteria hold and CI is green; merge to `main` only after S5, `/verify` and green CI (owner's rule). -* **File ownership prevents conflicts.** Only S2 edits `src/UltrafastEngine.Core/**`. S3 and S4 edit their own - `plugins//**`, `docs/plugins/.md`, their tests and their example folders. Shared files - (`mkdocs.yml`, `llms.txt`, `CHANGELOG.md`, `docs/testing.md`, `PROJECT_STATUS.md`, `docs/capabilities.md`, `ROADMAP.md`, - `docs/roadmap.md`) are edited by **the session that is merging**, last; until then a session writes its lines into - `docs/design/notes/.md`. S1 is allowed to touch `mkdocs.yml` and `llms.txt` (it moves pages). -* **Generated files** (`schemas/*.json`, `python/src/ultrafast/types.py`, `docs/reference/options.md`, `python-api.md`, - `validation-matrix.md`) are regenerated with `/regen` (and `--matrix`) once per merge; never edited by hand. -* **Definition of done** (from `CLAUDE.md`): builds for net10.0 and net48; new behaviour has a test with an analytic or - published reference (physics); full C# suite and Python tests pass after `build_engine.py`; generated files current; docs - (reference page, changelog, `docs/testing.md` row, `PROJECT_STATUS.md` counts); `mkdocs build --strict` with 0 warnings; - examples run with `ULTRAFAST_STRICT_OPTIONS=1`. -* **Honesty rules.** A disagreement with a reference is a finding, never tuned away; a test tolerance is not loosened to hide a - failure (the ePIE case of 2026-11-09 is the model: measure the spread first, then test what the algorithm guarantees); - numbers that are only round-off are printed as bounds; unverified constants are marked. -* **Python conventions** (all sessions): the package keeps full autocompletion (builders return the generated TypedDicts); - lowercase module names, CapWords classes; `ultrafast.build` keeps working; renamed things keep working through a shim with a - `DeprecationWarning`. -* **CI** runs on Linux (x64 and arm64) and Windows. macOS is checked by hand. Tests must not depend on a display, a GPU, ffmpeg - or the network. -* **Handoff.** Each session ends by committing, then writing `docs/design/notes/.md` (what is done, what is not, the - numbers measured, decisions taken, anything the next session must know), and listing its changelog lines there. - -## 4. Specifications shared by several sessions - -### 4.1 Python package structure (S1) - -Goal: someone reading `import ultrafast` can find things by what they *are*. Lowercase module names (PEP 8) with CapWords -classes: `ultrafast.system.Sequential`, `ultrafast.components.BeamSplitter`. If you prefer the capitalised style -(`Ultrafast.System`), say so before S1 starts; it is unusual for Python and would break `import ultrafast.system` -conventions of every tool, so I recommend lowercase. - -``` -ultrafast/ - __init__.py run, load, open, version; the commonly used names re-exported - build.py (kept) dict builders with TypedDict autocompletion; the low-level truth, unchanged API - system/ Sequential, Branched (ports, links, loops), load / save .ufs, validation, diagram - components/ classes or factories grouped by kind, each returning the typed dict: - optics (Mirror, Lens, Grating, GratingPair, Prism, Plate, Aperture, Mask, Wavefront), - splitters (BeamSplitter, PolarizingSplitter, Dichroic, LeakyMirror, Delay), - polarisation (Waveplate, Polarizer, Rotator, Faraday), - crystals, fibres, gain (Gain, Amplifier, Oscillator), media (Bulk, Gap) - sources/ Pulse, Beam, measured spectra, pedestals - instruments/ one class or factory per instrument (Spectrometer, Frog, DScan, Camera, WavefrontSensor, ...) - algorithms/ pure-numpy / engine-driving algorithms usable on their own: - retrieval (FROG, d-scan, Gerchberg-Saxton), optimisation (scans, Nelder-Mead, CMA-ES wrappers), - zernike (fit, evaluate, Noll tables), later holography and control (the `ao` plugin adds to it) - results/ result objects, metrics (duration, bandwidth, Strehl), saved-result loading - plotting/ split of the 1371-line plotting.py: style, temporal, spectral, spatial, zernike, instruments, - diagrams, animate (the old function names stay importable) - materials.py, crystal.py, cells.py, laser.py, measured.py, plugins.py (kept; thin re-exports where they move) -``` - -S1 starts by reading `build.py`, `core.py`, `plotting.py`, `plot.py`, `diagram.py`, `laser.py`, `__init__.py` and the Python -tests and writing the exact old-name → new-name table into `docs/design/notes/s1-foundation.md` **before moving anything**. -Rules: nothing public disappears (shims with `DeprecationWarning`); the TypedDict autocompletion test (existing) stays green; -a new test imports every old public name; `docs/reference/python.md` and `python-api.md` (generated) are updated. - -### 4.2 Plotting, Zernike decomposition and animation (S1) - -* **Consolidate** `plot.py`, `plotting.py`, `diagram.py` and the fitting plugin's `figures.py` into the `ultrafast.plotting` - package with one theme (light and dark), shared helpers (SI-prefix axes, dB, before/after and target/result panels, - convergence panels) and the old names as aliases. -* **Zernike decomposition plot** (`plotting.zernike`): input = a wavefront map (array with axes), a `wavefrontSensor` result, - or a complex field. Panels: the wavefront map; the fitted reconstruction; the residual; a bar chart of the Noll - coefficients (waves rms) coloured by radial order and labelled for the named modes (piston, tilt, defocus, astigmatism, coma, - trefoil, spherical); the rms and the Strehl estimate (Maréchal) in the title. Options: `max_noll`, piston/tilt removed or not, - one or several wavefronts side by side (for before/after correction), an animated variant that steps through a list of - wavefronts (used by the DM correction example). -* **Core?** *Checked:* the core already has `Zernike.Fit` and `Zernike.Z` (used by the `wavefrontSensor` instrument), so the - plot does **not** need new core work. Implement a small numpy `algorithms.zernike` (`noll_to_nm`, `evaluate`, `fit`) and - **cross-check it against the core's coefficients** for the same map in a test (1e-6), plus orthonormality (rms 1 over the - disc) as the analytic test. Do not duplicate more than that. -* **Animation helper** (`plotting.animate`): takes a frame function and a frame list, writes an **animated GIF with Pillow** - (no ffmpeg on the owner's Mac) and, when `ffmpeg` is on the path, an MP4 as well; options for fps, size, a loop pause on the - last frame, a progress bar. A budget check (`build/example_check.py`): a PNG over 300 kB or a GIF over 2.5 MB is flagged. - Tests: a 5-frame animation writes a valid GIF with the right frame count, size and loop flag; no display needed (Agg). - -### 4.3 Examples: grouped folders, an installable package, run-all scripts (S1) - -Today `docs/examples/` has 43 numbered folders. Regroup them (a **proposal**; S1 confirms each placement after reading its -README): - -| group | folders that move there (old number) | -|---|---| -| `GettingStarted/` | 00 quickstart, 01 spm-plate, 04 custom-index-python, 12 user-element, 18 custom-nonlinear-medium | -| `LaserSystems/` | 02 stretcher-compressor, 13 yb-multipass-cpa, 14 ti-sapphire-regen, 19 ti-sapphire-klm-haus, 20 yb-laser-system, 38 four-grating-compressor, 09 nopcpa-bbo, 42 noncollinear-opa, 22 contrast-chain | -| `Nonlinear/` | 03 mpc-argon, 06 carrier-resolved, 11 spatial-kerr, 24 filament, 25 supercontinuum, 27 ner-measured-input, 29 small-scale-self-focusing, 08 shg-lbo, 10 ppln-dfg | -| `Diffraction/` | 30 iris-diffraction, 35 tilt-fringes, 36 structured-beams | -| `Wavefronts/` | 16 position-dependent-index, 15 spatial-gain-thermal-lens (the AO examples join this group in S5) | -| `Branched/` | 31 cavity-and-coherence, 32 interferometers, 37 f2f-cep, 41 pump-probe | -| `Diagnostics/` | 17 instrument-tour, 33 modelled-autocorrelator, 34 modelled-instruments, 39 third-order-cross-correlator, 40 noncollinear-correlators, 21 measured-data, 28 delayed-light | -| `Components/` | 05 polarization-coatings, 23 components | -| `DigitalTwin/` | 26 transforms (the twin examples join this group in S5) | -| `Interop/` | 07 zemax-workflow, `cpp/` | -| `Computing/` | new (4.4) | -| `AdaptiveOptics/` | new, created empty-with-README in S1, filled in S5 | - -* **Constraints to preserve** (from `docs/CLAUDE.md`): example 09 reads example 08's results (both move; the path in 09 changes to - the new location), 26's `run-new-input.json` / `run-backward.json` use `results-fit/fitted-system.ufs`, tutorial specs use - `plate.ufs` and `cpa.ufs`. Do the move in **one `git mv` pass**, then fix: `mkdocs.yml` nav, every markdown link, the - `docs/CLAUDE.md` notes, `llms.txt`, `build/example_check.py` paths, the tutorials, `docs/testing.md`, CI paths, and every - "example NN" reference in the docs (keep a table `docs/examples/README.md` with old number → new path). `mkdocs --strict` and - `example_check.py` are the proof. -* **Run-all scripts** (the owner runs them locally; CI keeps `example_check.py` for the numbers): in every example folder a - `make_figures.py` that runs the example, writes its hero figure(s) and, for iterative algorithms, its animation(s) next to - it; the README references those files. A group runner `docs/examples//make_all.py` and a top-level - `docs/examples/make_all.py` with `--group`, `--only `, `--no-animations`, `--jobs N`, `--report report.json`, which - also checks the figure budget and prints a table (example, time, files written, problems). -* **Installable package.** *Fact:* a pip extra installs dependencies, not repository folders. So `ultrafast[examples]` pulls in a - separate package `ultrafast-examples` (the grouped folders as package data, plus the `ultrafast-examples` command: - `list`, `copy `, `run `). It is built locally (`build/package.py` gains `--examples-wheel`) and - resolved with `--find-links` like the plugin extras (no PyPI). The command copies examples to a writable folder, because - installed packages should not be written into. - -### 4.4 Optical-computing examples (S1; buildable today, no new core needed) - -`Computing/` shows what an optical system does in parallel and compares the engine's run time with the theoretical -processing time. Built from existing elements (`lens`, `mask` of kind `file`, cartesian grids, cameras): - -| example | what | theory to compare | -|---|---|---| -| `4f-correlator` | a 4f system with a matched filter in the Fourier plane: find a pattern in a scene in one pass | correlation theorem (cross-correlation by FFT); the peak position and height against `numpy` | -| `optical-convolution` | the same 4f layout as a convolution engine (blur, edge detection, Gaussian kernel) | convolution theorem; error versus `scipy`-free numpy reference | -| `matrix-vector` | a lens-based vector-matrix multiplier (cylindrical lenses and a mask, the classic Goodman layout) | y = Ax, error versus `A @ x`, with the number of operations the optics did | -| `parallelism-and-time` | scale the problem (N × N pixels) and plot **three times**: the engine's wall time, the digital FFT time of the same operation, and the *theoretical optical* time (propagation length / c plus detector integration time) | digital cost O(N² log N), optical cost independent of N once the system is built; a table and one plot | - -Each computing example prints the number of effective multiply-accumulate operations the optics performed (e.g. N² for a -convolution kernel of N² taps ⇒ N⁴ operations done in one light transit), the engine's measured wall time, and the optical -transit time, and states plainly that the engine *simulates* the optics on a digital machine, so its wall time is not the -physical time: the comparison is about scaling. A one-page README per example, one hero figure, and one animation (the scene, -the filter, and the output forming). - -### 4.5 The example quality bar (all sessions that write examples) - -| rule | detail | -|---|---| -| a hero figure | one figure that tells the story at a glance (before / after, target / result), consistent theme from `ultrafast.plotting` (light and dark), units on every axis, the analytic or target value on the same axes, ≤ 300 kB PNG | -| an animation for every iterative algorithm | GS, SPGD, modal search, phase locking, closed-loop correction, twin fits: the thing converging, with the metric curve beside it. 6-15 s, 15-20 fps, ≤ 2.5 MB | -| no hidden dependencies | GIF via Pillow; MP4 optional when `ffmpeg` exists | -| numbers match a fresh run | `/example-check`; round-off is printed as a bound | -| fast | under 2 minutes on a laptop CPU; the full-size SLM example under 5 minutes and says so | -| self-checking | ends by asserting the key number against its reference, so a regression breaks the check | -| showcase | `docs/gallery.md` and the README hero animation are assembled from the examples by `build/gallery.py` | - -## 5. The session briefs - -Each brief can be pasted as the first message of a fresh Claude Code session (after `/model` and, if available, the advisor -choice from section 2). Start every session in a new worktree on its own branch. Each brief ends with a **handoff**. - ---- - -### S1 Foundation (Sonnet 5.5, advisor Opus 5.5) - -**Goal.** Make the Python package and the examples easy to navigate and pleasant to look at, before anything new is added to -them: package structure, one plotting module with animation and a Zernike plot, grouped examples with an installable package -and run-all scripts, and the optical-computing examples. - -**Branch.** `dev/2026-11-s1-foundation` from `dev/2026-11`. - -**Read first, nothing else.** `CLAUDE.md`, `python/CLAUDE.md`, `docs/CLAUDE.md`, section 3 and 4.1-4.5 of this page, -`python/src/ultrafast/{__init__,build,core,plotting,plot,diagram,laser}.py` (line ranges for the big ones; never `types.py`), -`docs/examples/README.md`, `build/example_check.py`, `docs/reference/python.md`. - -**Files you own.** `python/**` (not the generated `types.py` / `python-api.md`), `docs/examples/**`, `docs/tutorials/**` (path -fixes only), `build/example_check.py`, `build/package.py` (the examples wheel), `build/gallery.py`, `mkdocs.yml` and -`llms.txt` (moves only). You do **not** touch `src/**` or `plugins/**` (except `plugins/fitting/python/.../figures.py` to fold -it into the plotting package, keeping its names). - -**Order of work.** -1. **The one advisor call (Opus 5.5), after reading**, before any edit: the old-name → new-name table (4.1) and the example regrouping table (4.3). -2. Python structure (4.1): create the packages, move code, add shims, add the import-every-old-name test. Keep - `ultrafast.build` as it is. -3. Plotting (4.2): split and consolidate, theme, helpers, `animate`, `algorithms.zernike`, the Zernike decomposition plot and - its tests (cross-check against the core's `wavefrontSensor` coefficients). -4. Regroup the examples in one `git mv` pass, fix every link and path (4.3), `mkdocs --strict`, `example_check.py` (all). -5. Per-example `make_figures.py`, group and top-level `make_all.py`, the budget check, `build/gallery.py`. -6. The examples package and the `ultrafast-examples` command (`build/package.py --examples-wheel`); `pip install` it into a - fresh venv and run `ultrafast-examples list`. -7. The `Computing/` examples (4.4) with their figures and animations; add them to the gallery. -8. Docs: `docs/reference/python.md` structure section, `docs/examples/README.md` (group index and old → new table), - changelog lines in the notes file. -9. Commit; no second advisor call (the budget is one); the tests and `mkdocs --strict` are the check. - -**Tests.** Import of every old public name; TypedDict autocompletion test still green; Zernike orthonormality and core -cross-check; GIF writer; figure-budget check; `ultrafast-examples copy` into a temp folder; the full Python suite after -`build_engine.py`; `example_check.py` 0 problems; `mkdocs --strict` 0 warnings. - -**Acceptance.** `python3 -c "from ultrafast.system import Sequential, Branched; from ultrafast.components import BeamSplitter"` -works; old imports work with a `DeprecationWarning`; `docs/examples/make_all.py --only Computing` produces every figure and -animation within budget; the example check passes with the new paths; the examples wheel installs and lists the groups. - -**Handoff.** `docs/design/notes/s1-foundation.md`: the old → new table, the example move table, anything left, the commands to -regenerate figures, the animation budget numbers. - -**Progress (2026-11-09, branch `dev/2026-11-s1-foundation`; details in `docs/design/notes/s1-foundation.md`).** - -| step | state | -|---|---| -| 1 advisor call (Opus 5.5) | done; its corrections are applied (public-surface snapshot first, no module named like a figure function, shim warns, fold decision written down) | -| 2 Python structure | **done**: `system`, `components` (optics, splitters, polarisation, crystals, fibres, `amplifiers`, media), `sources`, `instruments` (18 real factories), `algorithms`, `results`; CapWords names are aliases of the `build` functions, so typed returns are unchanged; builders kept in `build` only are listed in a test | -| 3 plotting | **done**: `plotting.py` split into a package (`style, data, time_domain, spectral, run_figures, spatial, instrument_panels, diagrams, zernike, animate`); every 2.2.0 public and underscore name still imports (`python/tests/data/public_surface_2_2_0.json`); `ultrafast.diagram` is a shim with a `DeprecationWarning`; matplotlib and Pillow stay lazy | -| Zernike and animation | **done**: `algorithms.zernike` (orthonormal to 1e-12 by polar quadrature; agrees with the engine's `wavefrontSensor` coefficients to 1e-6), the decomposition figure (one or several wavefronts, complex-field input, animated variant), GIF writer with a size-budget check | -| 4 regroup examples | **done**: one `git mv` pass into 10 groups (+ `Computing`, `AdaptiveOptics`); 138 files re-linked; old numbers kept in the `docs/examples/README.md` table; `example_check.py` takes a name, group or old number | -| 5 `make_figures.py`, `make_all.py`, budget check, `build/gallery.py` | **done**: a `make_figures.py` in every example (42), `make_all.py` per group and at the top (`--group --only --no-animations --jobs --report`); the figure budget is checked; `docs/gallery.md` is generated | -| 6 examples wheel and `ultrafast-examples` | **done**: `build/package.py --examples-wheel` (3.8 MB), `list` / `copy` / `run`, extras `animate` and `examples`; tested from a fresh venv | -| 7 optical-computing examples | **done**: `4f-correlator`, `optical-convolution`, `matrix-vector`, `parallelism-and-time` with hero figures and animations (all within budget) | -| 8 docs | **done**: package structure and Zernike / animation sections in `docs/reference/python.md`, regenerated `python-api.md`, `docs/examples/README.md`, architecture code map, releasing page, notes and changelog lines in `docs/design/notes/s1-foundation.md` | -| outside the brief (owner's requests) | **done**: brand CSS for the docs sidebar and links, long-logo footer on the plugin READMEs, `CONTRIBUTING.md`, `ROADMAP.md` and `tests/components/README.md`; Google Search Console verification tag in `overrides/main.html`; the `.gitignore` fix that let CI see `ultrafast.results` | - -Findings that change later sessions: - -* `lens` has no cylindrical or separate x/y focal length, so the Goodman vector-matrix multiplier of 4.4 cannot be built from - existing elements. The `matrix-vector` example will use a lens as an exact DFT matrix and a 4f system for circulant matrices; - a cylindrical lens is a note for S2. -* `plotting/zernike.py` and the fitting plugin's `figures.py`: the plugin figure stays in the plugin (it reads the fitting - result format and is registered only when the plugin loads); it uses the stable names re-exported by `ultrafast.plotting`. - This is the "fold" of 4.2, with the reason written down. -* Full verification of the finished S1 is recorded in `docs/design/notes/s1-foundation.md` (sections 9 and 10). **S1 was merged to `main` on 2026-11-09 (`a875aec`), before S5, at the owner's request;** S2 starts from `dev/2026-11` in a new session (what it should know first: section 11 of the notes). -* **Open for the owner:** repository size (section 8, point 1): only the four Computing animations (39-329 kB each) are committed so far; the other 38 examples draw a generic hero into a git-ignored `figures/` folder. - ---- - -### S2 Core and spikes (Opus 5.5, no advisor) - -**Goal.** Measure first, then build the few core primitives that the AO plugin, the tile array and the shapers need: -`modulator`, in-memory maps, streaming and sparse spectral sampling, a coherent tile source, a fast spatial path. Core physics -only: no device catalogue. - -**Branch.** `dev/2026-11-s2-core`. - -**Read first.** `CLAUDE.md`, `AGENTS.md`, `docs/concepts.md`, `docs/reference/transverse.md` (masks, profiles), -`docs/reference/spatial-properties.md`, `docs/reference/speed.md`, `docs/reference/components.md#aberrations`, -`src/UltrafastEngine.Core/Elements/MaskElement.cs` and `Transverse/*.cs` (line ranges), the plan page (B1-B4, B8), -the skills `engine-physics` and `physics-change`. - -**Files you own.** `src/UltrafastEngine.Core/**`, `src/UltrafastEngine.Native/**`, `tests/UltrafastEngine.Tests/**`, -`docs/reference/*` for the new elements, `docs/design/notes/spikes.md`. The generated files are regenerated once, at the end. - -**Order of work.** -1. **Spikes**, written up with numbers in `docs/design/notes/spikes.md`: - * **S1-scale:** cartesian profile 1920 × 1080 and 4096 × 4096, one frequency: time and memory of a `mask file`, one - propagation, one camera; CPU, GPU if present. - * **S2-sparse:** 800 nm / 30 fs through a phase mask: K = 4, 8, 16, 32 frequencies against a dense run; the error in focal peak - and pulse duration. - * **S3-chromatic:** a blazed grating as a `mask file` and as a thin `bulk` + `indexProfile`: does a pulse disperse like a - grating of the same period? - * **S4-loop:** time per trial of a 128² spatial run; compile-once-run-many potential. - * **S5-tiles:** 7 tiles as a Branched system against a coherent sum on a cartesian grid. - * **S6-multi-condition:** read `plugins/fitting/src/Fitting.cs` and report whether two datasets can be fitted jointly (input - for S4, not built here). - * **S7-repetition:** read how the amplifier handles a regenerative switch and round trips, and how `repRateHz` / steady state - is used (the input for the Pockels comment in section 6). -2. **Decide** (no advisor): with the spike numbers, choose streaming, sparse sampling and the chromatic rules, and write the - reasoning and the alternatives rejected into the notes file, because nobody reviews this choice before the owner does. **Gate G0: stop and show the owner the spike note if any spike contradicts the plan** (a full - 1920 × 1080 SLM at pulse bandwidth is the biggest risk). -3. **C1 `modulator`:** cell grid (count, pitch, offset, rotation), array of complex transmission / OPD / phase; - `fillFactor`, `crosstalkKernel`, `quantisationLevels`, `phaseWrapRad`; `chromatic`: `opd`, `phaseAtDesign`, `index`; - sub-pixel averaging (reuse the `mask` edge-averaging code). -4. **C2 in-memory maps:** arrays through the Python and C APIs and the run spec without files; a `map` callback; a compile - cache so a loop compiles once. -5. **C3 streaming and sparse spectral sampling** for linear spatial chains, with a reported error estimate. -6. **C4 coherent tile source** (`initialProfile` kind `tiles`). -7. **C5 fast path:** cw / narrow-band spatial runs; zonal sensor outputs (slope maps, wavefront map as a stable array). -8. **C6 GPU parity** of C1/C3/C4 on the spatial back-end (skipped without a device). -9. `physics-reviewer` on the diff, fix findings; `/regen`; merge wave: docs, changelog. - -**Tests (reference values).** Pure tilt moves the focus by f·θ; defocus gives the thin-lens shift; N-level blazed grating first -order [sin(π/N)/(π/N)]²; replica orders at λ/pitch; a ramp disperses a pulse like a grating (S3); streamed result equals the -all-frequency result (1e-12); the sparse error estimate bounds the true error found in S2; N in-phase tiles give N² peak, -phase error σ costs exp(−σ²), grating-lobe angle λ/pitch; cw equals the pulse at the carrier (1e-9); N trials equal N file runs -(1e-12). - -**Acceptance.** All of the above; full C# suite and Python tests green; net10.0 and net48 build; `mkdocs --strict`; the spike -note exists with numbers; a 1920 × 1080 single-frequency propagation runs within the budget the spike recorded. - -**Handoff.** `docs/design/notes/s2-core.md` with the API of each primitive as shipped (keys, units, defaults), the spike -numbers, and what the `ao` plugin may rely on. - -**Progress (2026-11-10, branch `dev/2026-11-s2-core`; details in `docs/design/notes/s2-core.md`, numbers and decisions in `docs/design/notes/spikes.md`).** - -| step | state | -|---|---| -| 1 spikes S1-S7 | **done**: S1-scale (1920 x 1080 cw 3.3 s / 1.05 GB; K = 16 32 s / 3.6 GB), S2-sparse (K = 8-16 give 1e-3 up to 30 rad), S3-chromatic (a wrapped map disperses as a grating), S4-loop (10 ms vs 46 ms per trial), S6, S7 read; **S5-tiles not run as a Branched system** (it cannot give an array's far field: tested against the array theorem instead) | -| 2 decide / gate G0 | **done**: no spike contradicts the plan; decisions in spikes.md section 8 | -| 3 C1 `modulator` | **done**: cell grid, offset, rotation, fill factor, gaps, quantisation, wrapping, `crosstalkSigmaCells` and `crosstalkKernel`, zero-order leak; `chromatic` `opd` (the brief's `phaseAtDesign`, a phase at the design wavelength scaled with omega, is accepted as an alias), `fixedPhase` (achromatic, geometric phase), `index` (Δn(λ) by Cauchy or table); 6 x 6 sub-pixel averaging (the `mask` approach) | -| 4 C2 in-memory maps | **done**: arrays, callbacks `"@name"` (Python `fields=`, C API `uf_run_set_field_callback`), NPZ; N loop trials = N file runs (1e-12). **Compile cache not built**: 10 ms floor per trial including the compile | -| 5 C3 streaming and sparse sampling | **done**: `transverse.frequencyBlock` (exact), `outputs.keepProfiles: "used"` (live set 1587 -> 336 MB), `spectralSamplingError` bounds the true error. Whole-executor streaming not built (not needed; peak is set by the GC) | -| 6 C4 tiles | **done**: square, hex, ring, explicit; piston, tilt, delay (sign tested), amplitudes, tophat | -| 7 C5 fast path, zonal outputs | **done**: `spectralSamples: 1` fixed to store the carrier (it was the band edge); slope maps (tilt, defocus) | -| 8 C6 GPU parity | **test written, skipped** on this Mac (no FP64 OpenCL, no PoCL wheel); its body checked CPU against CPU | -| 9 review, regen, docs | **done**: two `physics-reviewer` passes, `/regen`, reference pages, changelog, testing rows, status counts | - -Brief items met differently, so the owner can see them: "cw equals the pulse at the carrier (1e-9)" holds for the stored frequency (1e-7, one -grid bin); the cw source's own bin is 1.2e-6 off the requested carrier, so the patterns agree to 3e-6. The 1920 x 1080 budget is a slow test -(1.9 s on a 2048 x 1280 grid, limit 13 s). The cylindrical lens (S1 finding) already existed as `focalLengthXM` / `focalLengthYM`; it now has a test. - ---- - -### S3 AO plugin (Sonnet 5.5, advisor Opus 5.5) - -**Goal.** The `ao` plugin: deformable mirror, SLM, coherent tiled-aperture array; wavefront sensing with Zernike modal and -zonal reconstruction; calibration and control; holograms; sensorless optimisers and phase locking; temporal shaping -(`spectralShaper`, `aopdf`). All devices are **parametric**: no vendor data (owner decision 4), up to a full 1920 × 1080 SLM. - -**Branch.** `dev/2026-11-s3-ao` (after S2 is merged into `dev/2026-11`). - -**Read first.** `CLAUDE.md`, `plugins/CLAUDE.md`, `plugins/fitting/README.md` (as the model plugin), `docs/reference/plugins.md`, -the plan page B1-B9, `docs/design/notes/s2-core.md`, `docs/design/notes/spikes.md`, `docs/reference/retrieval.md` -(Gerchberg-Saxton section). - -**Files you own.** `plugins/ao/**` (new), `docs/plugins/ao.md`, `tests/UltrafastEngine.Tests/Ao*Tests.cs`, -`python/src/ultrafast/algorithms/` additions for holography and control (the Python side of the plugin). - -**Order of work** (package ids from the first version of this plan in brackets). -1. **Scaffold** (AO0): `plugin.json` (`requires: ["fitting"]` if the estimators are reused), the Python module, the docs page. -2. **Devices:** `deformableMirror` (AO1), `slm` (AO2), `tiledArray` (AO3), each compiling to the core `modulator` or tile source. - **Advisor call 1 (Opus 5.5)** before the SLM: the chromatic model (settled in S2) and the pulse test. -3. **Sensing and control:** `shackHartmann` model with Zernike modal and zonal (Southwell) reconstructors (AO4); `ao calibrate`, - `ao correct`, `ao loop` (AO5). -4. **Holograms** (AO6): GS, weighted GS, MRAF, complex-to-phase-only encoding (double-phase, Bolduc-type), - quantisation-aware refinement; **sensorless optimisers** (AO7): SPGD, hill climbing, modal search, phase locking, and - transmission matrix by phase stepping. -5. **Temporal shaping** (TS1, TS2): `spectralShaper` (pixels → wavelength, resolution kernel, fill factor, gaps, wrapping), - `aopdf` (spectral phase and amplitude, finite window, efficiency, ripple), closed-loop shaping with FROG or d-scan as the - metric. (TS3, the full 4f line with spatial chirp, is later.) -6. `physics-reviewer` on the diff; **advisor call 2 (Opus 5.5)** before done; docs, changelog lines in the notes file. - -**Tests (reference values).** DM influence at a neighbour equals the coupling; surface-to-wavefront factor 2 cos(aoi); Zernike -fit residual falls with the actuator count; Strehl of the corrected beam against Maréchal exp(−σ²); Shack-Hartmann spot shift = -f ∂W/∂x and noiseless Zernike recovery exact; SLM blazed-grating efficiency vs N levels; pixel replica orders at λ/pitch; a pulse -through a wrapped phase ramp; GS error monotone (Fienup 1982) and a known phase recovered; WGS spot uniformity; tile array N² -peak, exp(−σ²), delay-mismatch cost for a 30 fs pulse; focus enhancement (π/4)(N − 1) + 1 (Vellekoop and Mosk, Opt. Lett. 32, -2309 (2007)); programmed spectral phase delays the pulse; pixel-gap pre/post pulses at 1/(pixel bandwidth) (Weiner); the full -1920 × 1080 SLM example within the S1-scale budget. - -**Acceptance.** Every reference test passes; the plugin loads and lists its elements; docs page with a parameter table per -element (keys, units, defaults); no vendor numbers anywhere (presets are labelled generic). - -**Handoff.** `docs/design/notes/s3-ao.md`: the shipped API, the numbers, what the AO examples (S5) may call. - -**Progress (2026-11-10, branch `dev/2026-11-s3-ao`; details, numbers and the review in `docs/design/notes/s3-ao.md`, user docs in `docs/plugins/ao.md`).** - -| step | state | -|---|---| -| 1 scaffold | **done**: `plugins/ao`, sln, test reference, host copy targets, packager (`ao`) | -| 2 devices | **done**: `deformableMirror`, `slm` (compile to the core `modulator`); advisor call 1 settled the SLM model (Delta-n dispersion, grey-level curve, unquantised phases to the modulator, the `s(lambda)` test); `tiledArray` is a Python builder of the core tile source | -| 3 sensing and control | **done in Python**: lenslet Shack-Hartmann model, modal and Southwell reconstructors, interaction / control matrices, integrator loop, `DmBench`; no `ultrafast ao ...` CLI commands | -| 4 holograms, sensorless | **done**: GS (quantisation in the loop), WGS, MRAF, double-phase, Bolduc; SPGD, hill climb, modal search, transmission matrix; the (pi/4)(N - 1) + 1 enhancement is tested in NumPy only | -| 5 temporal shaping | **done**: `spectralShaper`, `aopdf`; closed-loop GDD compensation from the peak power; **FROG / d-scan as the metric not wired** | -| 6 review | **done**: `physics-reviewer` (15 findings, 9 fixed, the rest documented), advisor call 2 | - -Brief items met differently: the pulse through a wrapped phase ramp is three monochromatic runs (900, 1030, 1200 nm) against the `s(lambda)` law, not a pulse; the NativeAOT library does not compile the plugin in (core area); a shaper's side pulses and a large programmed GDD can wrap round the time window without a warning (planner hook, core area). - ---- - -### S4 Fitting and twin (Sonnet 5.5, advisor Opus 5.5) - -**Goal.** (a) Conditioned transforms in the **fitting** plugin (F1-F5): one set of shared parameters fitted to many operating -conditions, parametric and smooth-basis dependence on the condition vector, physics-constrained network transforms. -(b) A **`twin`** plugin on top (T1-T7): project layout, validation with held-out conditions, placement planner, experiment -design, what-if explorer, drift tracking, the MCP server and the goal file. - -**Branch.** `dev/2026-11-s4-twin`. Runs **in parallel with S2/S3**; it edits only `plugins/fitting`, `plugins/twin` and their docs. - -**Read first.** `CLAUDE.md`, `plugins/CLAUDE.md`, `docs/plugins/fitting.md`, `plugins/fitting/src/Fitting.cs` and -`Estimators.cs` (line ranges), `docs/examples/DigitalTwin/transforms/README.md` (the old example 26), the skills -`mirror-lab-setup` and `optimise-setup`, the plan page (C1-C6), `docs/agents.md`. - -**Files you own.** `plugins/fitting/**`, `plugins/twin/**` (new), `docs/plugins/fitting.md`, `docs/plugins/twin.md`, -`python/src/ultrafast_twin` or the plugin's Python module, `tests/UltrafastEngine.Tests/Fitting*Tests.cs`. **Not** `src/**`: -if the core needs a hook, write it in the notes file and let S2/S5 add it. - -**Order of work.** -1. **F1 multi-condition fit** (a `conditions` list; shared and per-condition parameters; held-out conditions; per-condition - residuals). **Advisor call 1 (Opus 5.5)** on the design after reading the code and the S6 spike result. -2. **F2 parametric conditioning** (constant, linear, power law, polynomial; physical priors), **F3 smooth bases in (ω, θ)** - (tensor-product splines or Chebyshev; penalties in both directions; the existing passivity, causality and - Kramers-Kronig constraints). -3. **F4 network transforms** (`kind: "network"`, a small MLP with constraints built into the output layer; trained by the - existing estimators; described in the docs as *physics-constrained network transforms*, not as a PINN); write the constraints - down in the notes file. **F5** is a short note on gradients through linear elements: measured, not promised. -4. **`twin` plugin:** T1 project layout and `ultrafast twin init / fit / validate`; T2 validation report with credible bands; - T3 placement planner and experiment design; T4 what-if explorer; T5 drift tracking (ensemble Kalman). -5. **T6 MCP server** (stdio, optional extra `ultrafast[mcp]`): about eight coarse tools (`describe`, `validate`, `run`, - `compare_to_measured`, `scan`, `fit`, `explain_warning`, `plot`), workspace-scoped, **user expressions and callbacks refused - by default**, a time and memory budget per call, read-only with respect to any lab hardware. **Advisor call 2** on the security - design before it is written. **T7** goal file and checker, and a small benchmark of design tasks with known answers. -6. Docs, changelog lines in the notes file, **advisor call 3** before done (the MCP surface is a security - boundary). - -**Tests (reference values).** Two synthetic conditions recover known shared parameters (1e-3); a held-out condition is -predicted within the noise; a nonlinear phase linear in energy recovers its slope from spectra at five energies; a known smooth -surface is recovered and the penalties stop noise fitting; the network recovers a hidden smooth map with its constraints -holding exactly; MCP: tool schema tests with a scripted client, and refusal tests (an expression, a Python callback, a path -outside the workspace, a call over its time budget). - -**Acceptance.** `ultrafast twin init` on the synthetic lab runs end to end; the validation report flags a twin that only fits -its training data; the MCP server passes its refusal tests; plugin docs complete. - -**Handoff.** `docs/design/notes/s4-twin.md`. - ---- - -### S5 Examples, docs, verification (Sonnet 5.5, advisor Opus 5.5) - -**Goal.** Fill `AdaptiveOptics/` and `DigitalTwin/` with examples that impress (hero figures and animations), finish the -documentation and verification, and merge to `main`. - -**Branch.** `dev/2026-11-s5-examples`. - -**Read first.** the notes files of S1-S4, section 4.5 of this page, `build/example_check.py`, `docs/examples/README.md`, -the plan page (B5, C4). - -**Files you own.** `docs/examples/AdaptiveOptics/**`, `docs/examples/DigitalTwin/**`, `docs/examples/Wavefronts/**` (additions), -the docs reference pages for the new elements, and (as the merging session) the shared files in section 3. - -**Order of work.** -1. **AdaptiveOptics:** E1 components gallery; E2 DM correction of a Zernike aberration (with the Zernike plot and the - correction animation); E3 sensorless correction (SPGD, modal search: spot sharpening animation); E4 aberrated high-power beam - with a DM and a nonlinear chain (B-integral maps with and without correction); E5 SLM beam shaping (smiley face, square, - ring, vortex donut; a **full 1920 × 1080 panel with 8-bit quantisation**; GS iteration animation); E6 complex shaping with a - phase-only device; E7 coherent tiled-aperture combining (7 and 61 tiles, SPGD locking animation, delay mismatch for a 30 fs - pulse); E8 temporal shaping (flat-top, double pulse, pulse train, pixel-gap side pulses, a gain-narrowed amplifier restored by - an AOPDF, closed-loop shaping animation); E9 transmission matrix through a scatterer. -2. **DigitalTwin:** E10 fitting across conditions (spectra at five energies, one shared transform, held-out energy); E11 build the - digital twin (synthetic lab with hidden aberrations; place transforms, fit five conditions, recover the hidden values, - predict a sixth with a band); E12 explore the twin without beam time; E13 network versus spline transform; E14 agent-driven - design through the MCP server; E15 the wavefront twin. -3. `make_figures.py` and `make_all.py` entries, gallery, README hero animation (`build/gallery.py`). -4. **Merge wave** (as the merging session): `CHANGELOG.md`, `docs/testing.md` rows, `PROJECT_STATUS.md` counts, - `docs/capabilities.md`, `ROADMAP.md`, `docs/roadmap.md`, `llms.txt`, `mkdocs.yml`; `/regen`; an "adaptive optics and digital - twin" section in `docs/agents.md`; the README badges (test counts). -5. `/verify` (builds, all tests, examples in strict mode, docs), CI green on Linux and Windows. **The one advisor call (Opus 5.5)** before the - merge. Merge `dev/2026-11` to `main` only then. A tag or release **only when the owner asks**. - -**Acceptance.** Every example runs under `ULTRAFAST_STRICT_OPTIONS=1`, asserts its key number, produces its figures and -animations within budget with `make_all.py`; `example_check.py` 0 problems; docs 0 warnings; CI green. - -**Handoff.** `docs/design/notes/s5-release.md`: counts, what was verified, what was not (GPU, OpticStudio, macOS by hand). - -**Aditional Note from the user**: Currently it seems that many of our examples are old, and are fairly unintuitive and a quick glance does not help. I think we need to really go through each and every example and tell a little story with some animations / nice plots - that is to say: for example with Wavefronts/ we want to see a 2d image of out inputs/output wavefronts! Or with LaserSystem/ show a range of results (ie wavefronts, contrast, FROG trace etc.). We also should add examples/Fibres to show our fibre capabilities. - -## 6. Repetition and Pockels cells (a comment for the plan; later session S6) - -*What the engine does today (checked in the docs).* Most of the engine is **single shot**: one pulse through a system. The gain -and amplifier elements are the exception: with a `repRateHz` (on the pulse, or on the element, which then acts as an **ideal -pulse picker**) they iterate the pump-amplify cycle to a **steady state** with a fast energy model, then propagate the last pulse -with the full model; period doubling is detected and warned about (`docs/reference/gain.md`, "Steady state at the repetition -rate"). A regenerative amplifier is a `geometry: "regenerative"` element with a number of round trips. The `grid.tail` second -grid carries delayed light such as ghosts and pre-pulses. Jones-calculus polarisation elements (waveplates, polarisers, -Faraday rotators) exist. - -*What I did not find (not checked beyond the docs; the S2 spike S7 reads the code to confirm).* A **switch model**: nothing in -the docs describes a Pockels cell, a pulse picker with a finite contrast, a switching window or its rise time, or the leakage -of a regenerative amplifier's cavity switch into neighbouring pulses. Time between pulses (ns to µs) is handled only as the -repetition period of the gain model, not as a resolved axis. - -*Why it matters.* A Pockels cell is the device that decides which pulse goes where: pulse pickers (rep-rate division), -regenerative-amplifier switch-in and switch-out, cavity dumping, and contrast cleaning (a plasma-mirror-free alternative in some -chains). Its imperfections are exactly the contrast problems the engine already cares about: **finite extinction ratio** (the -unselected pulses leak at, say, 1e-3 to 1e-2 of the selected one), **finite rise and fall time** (the selected pulse sees a -partial retardance if the switch edge is slow; neighbouring pre- and post-pulses see a rising edge), **voltage-dependent -retardance** (an electro-optic crystal acts as a waveplate with retardance π V / Vπ, plus thermal and piezoelectric ringing), -and in the regen the **round-trip number** setting the energy and the pre-pulse level. Repetition also matters beyond Pockels -cells: thermal lensing and its steady state depend on the average power; a DM or SLM responds at kHz or tens of Hz while -the laser runs at kHz to MHz, so an AO loop acts on averages, not on single shots; and tiled-array phase noise is a rate -question. - -*Design sketch (S6, not part of S1-S5).* -1. **`pockelsCell` element** (a voltage-controlled waveplate pair with a polariser, as a model rather than a vendor catalogue): - crystal (KD*P, BBO, RTP, LN) with Vπ, aperture, length, extinction ratio, an applied **voltage waveform** V(t) (rise time, - flat top, fall, a delay relative to the pulse), and the polarisation transform built from the existing Jones elements. On a - single pulse it applies the retardance at the pulse time, which the engine can already do with a `waveplate` of that - retardance; the new part is the time dependence across pulses. -2. **A pulse-train layer**: N pulses at the repetition period t_k, each passing the cell with transmission T(V(t_k)); the - per-pulse full propagation stays as it is, and a macro-time table records the energy, pre- and post-pulse leakage per pulse. - This is the same idea as the gain steady-state iteration (a fast model across pulses, the full model for the one that - matters), extended with a switch. -3. **A regenerative-amplifier switch**: switch-in and switch-out timing in terms of round trips with the cell's edge times, - so that the pre-pulse and the cavity-dump contrast come out of the model instead of being set by hand. -4. **A pulse-picker contrast example** (leakage into the contrast instrument) and a regen example with switching. - -*Open questions for the owner (S6, not blocking S1-S5).* Which switches matter first (pulse picker, regen, cavity dump)? -Is the contrast of the switch (extinction, edge time) a quantity you measure and want to fit with the twin? - -## 7. Risks and how the order handles them - -| risk | handled by | -|---|---| -| a full-size SLM does not fit a laptop | spikes S1-scale and S2-sparse before any design; streaming and sparse sampling in S2; examples carry a time and memory budget | -| the chromatic SLM model is wrong | spike S3 first; the blazed-grating-as-a-grating reference test before the SLM element exists; S2 runs on Opus 5.5 and records its reasoning for the owner; the S3 advisor re-reads it | -| the Python restructure breaks users | shims with `DeprecationWarning`, an import-every-old-name test, the autocompletion test | -| moving the examples breaks docs and CI | one `git mv` pass, an old → new table, `mkdocs --strict` and `example_check.py` as proof | -| multi-condition fitting is bigger than expected | spike S6-multi-condition reads the code first; F2 (parametric) ships before F4 (network) | -| networks cannot be trained at scale (no gradients) | F4 limited to small nets with the existing estimators; F5 is a note, not a promise | -| animations bloat the repository | budget check (≤ 2.5 MB each); decision below | -| MCP executes untrusted code | refused by default; workspace-scoped; read-only with respect to the lab; advisor review of the design | -| parallel sessions collide | file ownership (section 3); the merging session edits the shared files | -| token cost | model split in section 2, at most three advisor calls per session, `test-runner` on haiku, briefs that list what to read | - -## 8. Open points for the owner - -1. **Repository size for animations.** About 15 GIFs of up to 2.5 MB is about 40 MB. Alternatives: attach them to a release, or generate them in the docs workflow. Recommendation: commit the hero animations (a handful), generate the rest at docs build. -- GENERATE ALL IMAGES BEFORE BUILD AND COMMIT -1. **Gate G0** (inside S2) needs your review of the spike numbers. -2. **Python naming.** Lowercase modules (`ultrafast.system`) are recommended over the capitalised form; say so if you want the other. -- THAT IS FINE -3. **Version.** The merge is a feature release (2.3.0 by the project's convention); tags and releases only on your word. diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/README.md b/docs/examples/AdaptiveOptics/slm-beam-shaping/README.md index bf06911..6ff2d7b 100644 --- a/docs/examples/AdaptiveOptics/slm-beam-shaping/README.md +++ b/docs/examples/AdaptiveOptics/slm-beam-shaping/README.md @@ -16,8 +16,10 @@ Gerchberg-Saxton algorithm finds the phase: iterate between the SLM plane (a kno **Layout** (`slm_shaping.py`): Gaussian beam (1.4 mm radius, 800 nm) → `slm` (256 x 256 pixels of 20 µm, 256 linear grey levels) → lens f = N dx² / λ = 128 mm → camera. With this focal length the engine's focal-plane pixels are exactly the FFT bins of the -hologram, so the design and the engine result share one lattice. The "Fourier prediction" in the output is the orthonormal FFT of -the quantised phase times the |sinc|² envelope of the square pixel. +hologram, so the design and the engine result share one lattice. The panel is shifted by half a pixel (`offset_x_m`, `offset_y_m` = -pitch/2) so that its pixel +centres lie on the engine's grid points: the grid has a sample on the axis, an even panel has a pixel *edge* there, and without the shift every grid cell would average two +pixels. The reference in the output is an exact NumPy propagation of the same sampled field: Gaussian x quantised hologram x thin lens, angular spectrum over f +(`exact_prediction`). ```bash python slm_shaping.py @@ -26,18 +28,24 @@ python make_figures.py # optional: hero.png, quantisation.png, full-pan ``` panel 256 x 256 pixels of 20 um, 256 grey levels, lambda 800 nm, f = 128.0 mm (focal pixels = FFT bins), beam radius 1.4 mm -smiley energy on target pixels 0.94 (design 0.88), engine against the Fourier prediction (inside the window): correlation 0.9842 GS error 0.005 -> 0.001 -square energy on target pixels 0.95 (design 0.89), engine against the Fourier prediction (inside the window): correlation 0.9895 GS error 0.005 -> 0.001 -ring energy on target pixels 0.94 (design 0.88), engine against the Fourier prediction (inside the window): correlation 0.9935 GS error 0.005 -> 0.001 -spots energy on target pixels 0.42 (design 0.43), engine against the Fourier prediction (inside the window): correlation 0.9949 GS error 0.006 -> 0.003 +smiley energy on target pixels 0.90 (design 0.88); against the exact propagation of the written phase: open edge 0.009, absorbing edge 0.129 GS error 0.005 -> 0.001 +square energy on target pixels 0.91 (design 0.89); against the exact propagation of the written phase: open edge 0.009, absorbing edge 0.138 GS error 0.005 -> 0.001 +ring energy on target pixels 0.90 (design 0.87); against the exact propagation of the written phase: open edge 0.010, absorbing edge 0.117 GS error 0.005 -> 0.001 +spots energy on target pixels 0.41 (design 0.43); against the exact propagation of the written phase: open edge 0.000, absorbing edge 0.056 GS error 0.006 -> 0.003 GS error is non-increasing after the first iterations (Fienup 1982) checks passed ``` -**What it shows.** 94-95 % of the light lands on the pixels of the smiley, the square and the ring (the GS design predicts 88-89 %, -the engine's pixel averaging and the absorbing edge move a few percent of the light around). The engine reproduces the Fourier -prediction of the written phase with a correlation of 0.98-0.99. The sharper the target, the more light a GS hologram leaves -elsewhere: the weighted-GS spot array puts 42 % of the light into the nine spots. +**What it shows.** 90-91 % of the light lands on the pixels of the smiley, the square and the ring (the GS design predicts 87-89 %). The sharper the target, the more +light a GS hologram leaves elsewhere: the weighted-GS spot array puts 41 % of the light into the nine spots. + +**What was checked, and a finding.** The numbers after "against the exact propagation" are the relative L2 distance between the engine's focal fluence and the exact +propagation of the same sampled field (both normalised to unit sum). With the engine's absorbing edge switched off the distance is **0.9-1.0 %** (a bound; its cause is not separated further). With the absorbing edge on, which is the engine's default, it is 12-14 %: the periodic +grid wraps the diffraction orders of the pixelated hologram round the window, and the edge absorbs them, so the engine shows the isolated pattern and the periodic +reference the pattern plus its replicas. The earlier version of this example compared with a plain FFT and reported a correlation of 0.98-0.99; the missing +light had two causes found in this check: (1) the panel was half a pixel off the grid, so each grid cell averaged two pixels (an ideal blaze of P pixels per period then keeps only +cos²(π/P) of its light in the engine: 0.854 at P = 8, 0.962 at P = 16, exactly the values measured before the fix; with the shift it keeps 1.0000), and (2) the +absorbing edge. The engine now warns when the device pitch equals the grid step and the cell edges fall on grid points. Quantisation: `quantisation.png` compares the GS error with an ideal phase, 256 levels and 4 levels, and shows the far field of a 4-level hologram next to the 256-level one through the engine. The S3 measurements found that quantisation inside the loop helps at @@ -47,7 +55,7 @@ Quantisation: `quantisation.png` compares the GS error with an ideal phase, 256 **The full panel.** `python slm_shaping.py --full` designs the smiley for a **1920 x 1080** panel (8 µm pixels, 8 bit) on the 2048 x 2048 grid the engine uses and propagates it (about 15 s on a laptop; the panel's phase goes in as a callback, not as JSON): -91 % of the light on the smiley (the GS design predicts 86 %). +87 % of the light on the smiley (the GS design predicts 86 %). ![full panel](full-panel.png) diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/animation.gif b/docs/examples/AdaptiveOptics/slm-beam-shaping/animation.gif index e8417d9..a1033e9 100644 Binary files a/docs/examples/AdaptiveOptics/slm-beam-shaping/animation.gif and b/docs/examples/AdaptiveOptics/slm-beam-shaping/animation.gif differ diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/full-panel.png b/docs/examples/AdaptiveOptics/slm-beam-shaping/full-panel.png index 716a624..faac4b1 100644 Binary files a/docs/examples/AdaptiveOptics/slm-beam-shaping/full-panel.png and b/docs/examples/AdaptiveOptics/slm-beam-shaping/full-panel.png differ diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/hero.png b/docs/examples/AdaptiveOptics/slm-beam-shaping/hero.png index e7d78fa..7fe5c75 100644 Binary files a/docs/examples/AdaptiveOptics/slm-beam-shaping/hero.png and b/docs/examples/AdaptiveOptics/slm-beam-shaping/hero.png differ diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/quantisation.png b/docs/examples/AdaptiveOptics/slm-beam-shaping/quantisation.png index 53def1c..82e4bf7 100644 Binary files a/docs/examples/AdaptiveOptics/slm-beam-shaping/quantisation.png and b/docs/examples/AdaptiveOptics/slm-beam-shaping/quantisation.png differ diff --git a/docs/examples/AdaptiveOptics/slm-beam-shaping/slm_shaping.py b/docs/examples/AdaptiveOptics/slm-beam-shaping/slm_shaping.py index e8316da..5b1f658 100644 --- a/docs/examples/AdaptiveOptics/slm-beam-shaping/slm_shaping.py +++ b/docs/examples/AdaptiveOptics/slm-beam-shaping/slm_shaping.py @@ -51,16 +51,17 @@ def spots(n): def illumination(n, dx, w=W): - X = (np.arange(n) - (n - 1) / 2) * dx + X = (np.arange(n) - n / 2) * dx # the engine's grid points: a sample on the axis, pixel i of the panel on grid point i return np.exp(-(X[:, None] ** 2 + X[None, :] ** 2) / w ** 2) -def engine_focus(phase, n, dx, lam=LAM, f=None, slm_n=None): - """Write ``phase`` to an 8-bit SLM (256 grey levels, linear 0..2 pi) and return the engine's focal-plane fluence map and axes.""" +def engine_focus(phase, n, dx, lam=LAM, f=None, absorbing=True): + """Write ``phase`` to an 8-bit SLM (256 grey levels, linear 0..2 pi) and return the engine's focal-plane fluence map and axes. The panel is shifted by half a pixel so that its + pixel centres lie on the engine's grid points (the grid has a sample on the axis, an even panel has a pixel edge there; without the shift every grid cell averages two pixels).""" f = f or critical_focal_length(n, dx, lam) - g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * dx / 2, spectral_samples=1, absorbing_boundary=True, initial_profile=ip) + g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * dx / 2, spectral_samples=1, absorbing_boundary=absorbing, initial_profile=ip) sysd = b.system("slm-shaping", [b.gap("src", 0.0, medium="Vacuum", transverse=g(b.initial_profile("gaussian", radius_m=W))), - Slm("slm", dx, pixels_x=n, pixels_y=n, phase_target_rad=phase, grey_levels=256, dispersion="none"), + Slm("slm", dx, pixels_x=n, pixels_y=n, phase_target_rad=phase, grey_levels=256, dispersion="none", offset_x_m=-dx / 2, offset_y_m=-dx / 2), b.lens("lens", f), b.gap("z", f, medium="Vacuum", transverse=g())]) d = uf.run(sysd, b.run_spec(b.cw(lam, 1.0), instruments=[b.instrument("beamProfile", at="z", name="cam")])).instrument("cam") return np.asarray(d["fluence_map_j_m2"]), np.asarray(d["x_m"]), np.asarray(d["y_m"]) @@ -70,13 +71,25 @@ def design(target, iterations=60, seed=1, levels=256): return holography.gerchberg_saxton(target, illumination(target.shape[0], DX), iterations, seed=seed, levels=levels) -def fourier_prediction(phase, levels=256): - """What the written (quantised) phase should give: |FFT|^2 times the pixel-aperture envelope, on the same lattice, orthonormal, fftshifted.""" +def exact_prediction(phase, levels=256, lam=LAM, dx=DX): + """What the written (quantised) phase gives: the sampled field (Gaussian x e^{i phase}) times the thin lens, propagated over f with the exact angular spectrum + (NumPy, periodic grid, no absorbing edge), on the engine's lattice. Returns the fluence, normalised to unit sum.""" n = phase.shape[0] - q = holography.quantise_phase(phase, levels) - X, Y = np.meshgrid(np.arange(n) - n / 2, np.arange(n) - n / 2, indexing="ij") - pixel = (np.sinc(X / n) * np.sinc(Y / n)) ** 2 # the square pixel aperture: |sinc|^2 envelope across the focal window - return pixel * np.abs(holography.focal_field(q, illumination(n, DX))) ** 2 + f = critical_focal_length(n, dx, lam) + X = (np.arange(n) - n / 2) * dx + XX, YY = np.meshgrid(X, X, indexing="ij") + field = illumination(n, dx) * np.exp(1j * holography.quantise_phase(phase, levels)) * np.exp(-1j * np.pi * (XX ** 2 + YY ** 2) / (lam * f)) + fx = np.fft.fftfreq(n, dx) + FX, FY = np.meshgrid(fx, fx, indexing="ij") + h = np.exp(2j * np.pi / lam * f * np.sqrt(1 - (lam * FX) ** 2 - (lam * FY) ** 2 + 0j)) + p = np.abs(np.fft.ifft2(np.fft.fft2(field) * h)) ** 2 + return p / p.sum() + + +def rel_l2(a, b): + """Relative L2 distance of two fluence maps after normalising each to unit sum.""" + a, b = a / a.sum(), b / b.sum() + return float(np.linalg.norm(a - b) / np.linalg.norm(b)) def run(): @@ -85,11 +98,10 @@ def run(): for name, t in targets.items(): h = design(t) F, x, y = engine_focus(h.phase, N, DX) - P = fourier_prediction(h.phase) - out[name] = {"target": t, "hologram": h, "fluence": F, "x": x, "prediction": P} + out[name] = {"target": t, "hologram": h, "fluence": F, "x": x, "open": engine_focus(h.phase, N, DX, absorbing=False)[0], "prediction": exact_prediction(h.phase)} wgs = holography.weighted_gs(spots(N), 30, source_amplitude=illumination(N, DX), seed=3) F, x, y = engine_focus(wgs.phase, N, DX) - out["spots"] = {"target": spots(N), "hologram": wgs, "fluence": F, "x": x, "prediction": fourier_prediction(wgs.phase)} + out["spots"] = {"target": spots(N), "hologram": wgs, "fluence": F, "x": x, "open": engine_focus(wgs.phase, N, DX, absorbing=False)[0], "prediction": exact_prediction(wgs.phase)} return out @@ -103,12 +115,11 @@ def main(): % (N, N, DX * 1e6, LAM * 1e9, critical_focal_length(N, DX) * 1e3, W * 1e3)) for name, d in r.items(): t, F, P, h = d["target"], d["fluence"], d["prediction"], d["hologram"] - n = F.shape[0]; X, Y = np.meshgrid(np.arange(n) - n / 2, np.arange(n) - n / 2, indexing="ij"); inner = np.hypot(X, Y) <= n / 2 # light the periodic FFT wraps round is absorbed at the grid edge - c = np.corrcoef(F[inner], P[inner])[0, 1] + e_open, e_abs = rel_l2(d["open"], P), rel_l2(F, P) mono = "" if not h.error else (" GS error %.3f -> %.3f" % (h.error[0], h.error[-1])) - print("%-7s energy on target pixels %.2f (design %.2f), engine against the Fourier prediction (inside the window): correlation %.4f%s" - % (name, energy_on_target(F, t), h.efficiency, c, mono)) - assert c > 0.97, "the engine must reproduce the Fourier transform of the written phase" + print("%-7s energy on target pixels %.2f (design %.2f); against the exact propagation of the written phase: open edge %.3f, absorbing edge %.3f%s" + % (name, energy_on_target(F, t), h.efficiency, e_open, e_abs, mono)) + assert e_open < 0.02, "the engine must reproduce the exact angular-spectrum propagation of the written phase" assert energy_on_target(F, t) > 0.5 * h.efficiency err = r["smiley"]["hologram"].error assert all(err[i + 1] <= err[i] * 1.001 for i in range(len(err) - 1) if i > 2), "GS error should not grow (Fienup 1982)" @@ -137,7 +148,7 @@ def holo(x, y, z): return panel[np.clip(i, 0, px - 1), np.clip(j, 0, py - 1)] g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * dx / 2, spectral_samples=1, absorbing_boundary=True, initial_profile=ip) sysd = b.system("slm-full", [b.gap("src", 0.0, medium="Vacuum", transverse=g(b.initial_profile("gaussian", radius_m=3.0e-3))), - Slm("slm", dx, pixels_x=px, pixels_y=py, phase_target_rad="@holo", grey_levels=256, dispersion="none"), + Slm("slm", dx, pixels_x=px, pixels_y=py, phase_target_rad="@holo", grey_levels=256, dispersion="none", offset_x_m=-dx / 2, offset_y_m=-dx / 2), b.lens("lens", f), b.gap("z", f, medium="Vacuum", transverse=g())]) spec = b.run_spec(b.cw(LAM, 1.0), instruments=[b.instrument("beamProfile", at="z", name="cam")]) t0 = time.time() diff --git a/docs/examples/AdaptiveOptics/temporal-shaping/README.md b/docs/examples/AdaptiveOptics/temporal-shaping/README.md index f783890..9e7df93 100644 --- a/docs/examples/AdaptiveOptics/temporal-shaping/README.md +++ b/docs/examples/AdaptiveOptics/temporal-shaping/README.md @@ -25,7 +25,7 @@ input pulse: 30.0 fs FWHM, 800 nm double pulse (aopdf replica, 150 fs): peaks at [-75.0, 75.0] fs, height ratio 1.000 3-pulse train (shaper, 120 fs): 3 pulses spaced 120 fs stretched (GDD 6000 fs^2): FWHM 555 fs (stationary phase: GDD x bandwidth = 554 fs) -pixel gaps (fill factor 0.8, pixel 500 um): side pulse at 4.27 ps (2 pi / pixel bandwidth = 4.27 ps), energy ratio 0.0159 (formula 0.0159) +pixel gaps (fill factor 0.8, pixel 500 um): side pulse at 4.28 ps (2 pi / pixel bandwidth = 4.27 ps), energy ratio 0.0159 (formula 0.0159) gain narrowing: FWHM 15.0 fs (broad input) -> 39.3 fs (narrowed) -> 29.3 fs (restored by the AOPDF amplitude table) closed loop, unknown chirp GDD 900 fs^2 and TOD 4000 fs^3: peak power per sweep (fraction of the transform limit): 0.34 0.41 0.53 0.67 0.85 0.91 0.97 1.00 1.00 found GDD -900 fs^2, TOD -4018 fs^3 @@ -38,9 +38,9 @@ checks passed * **Pulse train**: the shaper's mask is the transfer function `Σ_k exp(iωkτ)/3` (a phase and an amplitude per pixel, 1024 pixels over 120 nm) and gives three pulses 120 fs apart. * **Stretching**: a GDD of 6000 fs² stretches 30 fs to 555 fs, as `GDD x bandwidth` predicts (554 fs). -* **Pixel gaps**: a mask with fill factor 0.8 and 0.5 nm pixels leaves a side pulse at 4.27 ps = 2π / pixel bandwidth, with the energy - ratio `[sin(πf)/(πf)]² exp(-2(π w₀/p)²)` = 0.0159 (Weiner, Rev. Sci. Instrum. 71, 1929 (2000)); measured 0.0159. The run sets - `grid.windowS` to 40 ps because the planner does not know about side pulses. +* **Pixel gaps**: a mask with fill factor 0.8 and 0.5 nm pixels leaves a side pulse at 4.28 ps (one grid step from the exact 4.27 ps) = 2π / pixel bandwidth, with the energy + ratio `[sin(πf)/(πf)]² exp(-2(π w₀/p)²)` = 0.0159 (Weiner, Rev. Sci. Instrum. 71, 1929 (2000)); measured 0.0159. The run does not + set `grid.windowS`: the grid planner makes room for the side pulses. * **Gain narrowing restored**: a Gaussian amplitude filter (a stand-in for gain narrowing, not a gain model) stretches a 15 fs pulse to 39 fs; an AOPDF amplitude table that is the inverse of the filter, limited to a factor 4 so that it stays passive, brings it back to 29 fs, not 15 fs: the limit on the correction is the dynamic range, not the device. diff --git a/docs/examples/AdaptiveOptics/temporal-shaping/temporal_shaping.py b/docs/examples/AdaptiveOptics/temporal-shaping/temporal_shaping.py index c95584c..28feb6e 100644 --- a/docs/examples/AdaptiveOptics/temporal-shaping/temporal_shaping.py +++ b/docs/examples/AdaptiveOptics/temporal-shaping/temporal_shaping.py @@ -15,7 +15,7 @@ def go(elements, pulse=None, window=WINDOW): pulse = pulse or b.pulse(LAM, TP, energy=1e-6) - r = uf.run(b.system("shape", [b.gap("g", 0.0, medium="Vacuum")] + elements), b.run_spec(pulse, grid={"windowS": window})) + r = uf.run(b.system("shape", [b.gap("g", 0.0, medium="Vacuum")] + elements), b.run_spec(pulse, grid={"windowS": window} if window else {})) return {"t": r["output_time_s"], "p": r["output_power_w"], "lam": r["output_wavelength_m"], "s": r["output_spectrum"], "sum": r.summary["output"]} @@ -63,7 +63,7 @@ def run(): out["flat"] = go([aopdf(gddFs2=6000.0)], window=4e-12) pix = 0.5e-3 # 0.5 mm of mask = 0.5 nm of spectrum: 12 pixels across the 150 fs pulse out["gaps"] = {"fill": 0.8, **go([SpectralShaper("px", pixels=60, pixel_pitch_m=pix, dispersion_m_per_m=1e6, spot_radius_m=0.25 * pix, fill_factor=0.8, gap_transmission=0.0, - center_wavelength_m=LAM)], pulse=b.pulse(LAM, 150e-15, energy=1e-6), window=40e-12)} + center_wavelength_m=LAM)], pulse=b.pulse(LAM, 150e-15, energy=1e-6), window=None)} out["pix"] = pix # gain narrowing: a Gaussian amplitude filter (stands in for the gain), then an amplitude table that undoes it wl = np.linspace(700e-9, 900e-9, 201) diff --git a/docs/examples/AdaptiveOptics/tiled-array/README.md b/docs/examples/AdaptiveOptics/tiled-array/README.md index 0e8b0fa..0af1f24 100644 --- a/docs/examples/AdaptiveOptics/tiled-array/README.md +++ b/docs/examples/AdaptiveOptics/tiled-array/README.md @@ -32,7 +32,7 @@ delay mismatch of the 30 fs tiles: axis fluence over the aligned value (engine, rms 25 fs 0.707 0.712 rms 40 fs 0.481 0.472 rms 60 fs 0.535 0.532 -SPGD locking of the 7 pistons: axis fluence 0.16 -> 1.02 of the in-phase value in 120 iterations (360 engine runs) +SPGD locking of the 7 pistons: axis fluence 0.04 -> 1.00 of the in-phase value in 120 iterations (360 engine runs) checks passed ``` @@ -45,9 +45,13 @@ checks passed the engine follows the Gaussian-overlap formula `(1/N²) Σ exp(-ln2 (τ_k - τ_l)²/τ_p²)` to 2 %. For the 7 random delays drawn at 60 fs rms the value rises again (0.535): it is a sample of seven, not an average. Timing, not just phase, matters for few-cycle pulses. -* SPGD (gain 10, shake 0.2 rad) takes the pinhole signal from 0.16 to the in-phase value in about 60 iterations. The final ratio is - slightly above 1 (1.02) on this 128-point grid: the camera pixel on the axis is not exactly the axis maximum; that small excess is - not explained further here and is within the example's 0.95 bound. +* SPGD (gain 10, shake 0.2 rad) takes the pinhole signal from 0.04 to the in-phase value in about 60 iterations. The loop runs on 128 x 128 cells with a lens of + f = 1 m. **Finding (the earlier version of this example ended at 1.02 of the in-phase value):** at f = 0.5 m the lens phase of a tile 3 mm off the axis changes by 3.7 rad per cell on + 128 cells, more than π, so the lens is under-sampled, and the seven identical tiles reach the axis with fluences that differ by a factor of 56 (one tile alone: 1, 1, 0.08, 4.6, 0.08, 1, 1 + times the central tile's). The zero-piston sum was then not the maximum, and SPGD found a better one. At f = 1 m (1.9 rad per cell) the seven fluences are equal to 1e-3, and so is + the case of 256 cells at f = 0.5 m, which the other runs of this example use; the lock then ends at the in-phase value and not above it, and the example asserts both bounds + (`test_tiled_array_identical_tiles_reach_the_axis_alike_only_when_the_lens_is_resolved`). A NumPy model of the sampled field does not reproduce the engine's weights in the aliased case, which + is why the cause is stated as the under-sampled lens (the symptom, equal tiles reaching the axis unequally, is measured) and not decomposed further. **Limits.** Tiles are identical Gaussians of one radius: no fibre mode-field profile, no coupling between cores, no per-channel dispersion or nonlinearity beyond a delay. The engine normalises the total power. diff --git a/docs/examples/AdaptiveOptics/tiled-array/animation.gif b/docs/examples/AdaptiveOptics/tiled-array/animation.gif index 4041c87..891cc43 100644 Binary files a/docs/examples/AdaptiveOptics/tiled-array/animation.gif and b/docs/examples/AdaptiveOptics/tiled-array/animation.gif differ diff --git a/docs/examples/AdaptiveOptics/tiled-array/hero.png b/docs/examples/AdaptiveOptics/tiled-array/hero.png index 910e2a6..6836960 100644 Binary files a/docs/examples/AdaptiveOptics/tiled-array/hero.png and b/docs/examples/AdaptiveOptics/tiled-array/hero.png differ diff --git a/docs/examples/AdaptiveOptics/tiled-array/make_figures.py b/docs/examples/AdaptiveOptics/tiled-array/make_figures.py index 962f740..853c110 100644 --- a/docs/examples/AdaptiveOptics/tiled-array/make_figures.py +++ b/docs/examples/AdaptiveOptics/tiled-array/make_figures.py @@ -35,7 +35,7 @@ ax[1, 1].plot(np.arange(1, len(h) + 1), h, color=th[0]); ax[1, 1].axhline(1, color="0.6", ls="--", lw=1) ax[1, 1].set_xlabel("SPGD iteration"); ax[1, 1].set_ylabel("axis fluence / in phase"); ax[1, 1].set_title("phase locking (3 runs per iteration)") n128 = lk["ref"].shape[0] -mk = lambda u: ex.focus(ex.array(1, piston_rad=(lk["phi0"] + u).tolist()), cw, n=128) +mk = lambda u: ex.focus(ex.array(1, piston_rad=(lk["phi0"] + u).tolist()), cw, n=128, f=1.0) F0, F1 = mk(np.zeros(7))[0], mk(lk["result"].x)[0] xs = mk(np.zeros(7))[1] V.image(ax[1, 2], V.crop(F0, xs, xs, 1e-3)[0], "random pistons: %.2f" % lk["start"], extent=[-1000, 1000] * 2, vmax=lk["ref"].max(), colorbar=False) diff --git a/docs/examples/AdaptiveOptics/tiled-array/tiled_array.py b/docs/examples/AdaptiveOptics/tiled-array/tiled_array.py index 1c9642a..c1b55f9 100644 --- a/docs/examples/AdaptiveOptics/tiled-array/tiled_array.py +++ b/docs/examples/AdaptiveOptics/tiled-array/tiled_array.py @@ -54,13 +54,14 @@ def run(): D = tau[:, None] - tau[None, :] rows.append({"rms_fs": rms * 1e15, "engine": v[c, c] / refp[c, c], "formula": float(np.exp(-np.log(2) * D ** 2 / TP ** 2).sum() / 49)}) out["delay"] = rows - # SPGD locking, 128 x 128 cells so that every trial is quick - refl, _, _ = focus(array(1), cw, n=128) + # SPGD locking, 128 x 128 cells so that every trial is quick. The lens is f = 1 m here: at f = 0.5 m the lens phase at the outer tiles (3.7 rad per cell at 3 mm) is + # under-sampled on 128 cells, the outer tiles reach the axis with other amplitudes and phases, and the zero-piston sum is not the maximum (1.015 x it, README) + refl, _, _ = focus(array(1), cw, n=128, f=1.0) phi0 = np.random.default_rng(3).uniform(0, 2 * np.pi, 7) cl = 64 def metric(u): - return focus(array(1, piston_rad=(phi0 + u).tolist()), cw, n=128)[0][cl, cl] / refl[cl, cl] + return focus(array(1, piston_rad=(phi0 + u).tolist()), cw, n=128, f=1.0)[0][cl, cl] / refl[cl, cl] start = metric(np.zeros(7)) t0 = time.time() lock = sensorless.spgd(metric, np.zeros(7), iterations=120, gain=10.0, amplitude=0.2, seed=1, keep_iterates=True) @@ -85,7 +86,7 @@ def main(): assert abs(ip["gain"] / 7 - 1) < 0.01 and abs(r["n61"]["gain"] / 61 - 1) < 0.02 assert abs(ip["piston_ratio"] / ip["piston_theory"] - 1) < 0.01 assert all(abs(d["engine"] / d["formula"] - 1) < 0.02 for d in r["delay"]) - assert k["start"] < 0.3 and k["result"].history[-1] > 0.95 + assert k["start"] < 0.3 and 0.95 < k["result"].history[-1] < 1.005, "the lock must reach the in-phase value and not exceed it (the zero-piston sum is the maximum when the lens is resolved)" print("checks passed") diff --git a/docs/examples/Branched/interferometers/README.md b/docs/examples/Branched/interferometers/README.md index b3d7db0..6c33012 100644 --- a/docs/examples/Branched/interferometers/README.md +++ b/docs/examples/Branched/interferometers/README.md @@ -15,9 +15,9 @@ python interferometers.py Mach-Zehnder output / input 0.5004 analytic 0.5004 spectral fringes largest error below 1e-12 of the peak budget closure below 1e-12 of the input -etalon peak 1.0000 Airy 1.0000, largest error 3.5e-08 +etalon peak 0.9996 Airy 0.9996, largest error 3.3e-08 ``` ![Spectral fringes of the Mach-Zehnder, and the etalon against the Airy function](interferometers.png) -The Airy function uses the grid's carrier, `summary["outputGrid"]["omega0"]`, because the grid moves the requested wavelength slightly so that the carrier falls on a sample. +The Airy function uses the grid's carrier, `summary["outputGrid"]["omega0"]`; a cw source sits exactly on the requested wavelength, so it is that wavelength. The peak is 0.9996, not 1, because the 81 scan points do not fall exactly on the resonance (the round trip is about 19 400 waves long, so the position of the peak within the scan depends on the sixth digit of the carrier). Engine and Airy function agree equally well at every point. diff --git a/docs/examples/Branched/interferometers/interferometers.png b/docs/examples/Branched/interferometers/interferometers.png index 7dd3cb6..0e7c59f 100644 Binary files a/docs/examples/Branched/interferometers/interferometers.png and b/docs/examples/Branched/interferometers/interferometers.png differ diff --git a/docs/examples/Diagnostics/modelled-autocorrelator/autocorrelator.png b/docs/examples/Diagnostics/modelled-autocorrelator/autocorrelator.png index 2b9f635..b4392fb 100644 Binary files a/docs/examples/Diagnostics/modelled-autocorrelator/autocorrelator.png and b/docs/examples/Diagnostics/modelled-autocorrelator/autocorrelator.png differ diff --git a/docs/examples/Diagnostics/modelled-instruments/instruments.png b/docs/examples/Diagnostics/modelled-instruments/instruments.png index 6c0fb2a..68759fc 100644 Binary files a/docs/examples/Diagnostics/modelled-instruments/instruments.png and b/docs/examples/Diagnostics/modelled-instruments/instruments.png differ diff --git a/docs/examples/DigitalTwin/README.md b/docs/examples/DigitalTwin/README.md index eecabf7..c5673fd 100644 --- a/docs/examples/DigitalTwin/README.md +++ b/docs/examples/DigitalTwin/README.md @@ -13,4 +13,4 @@ Run: `python docs/examples/make_all.py --group DigitalTwin`. Plugin pages: [fitt **Not here yet:** a comparison of a spline and a network transform (the network transform exists and is tested in the fitting plugin), a wavefront twin (needs a spatial project template that does not exist yet), and agent-driven design through an MCP server (the MCP -server is not built; the design is in [the S4 notes](../../design/notes/s4-twin.md)). +server is not built; the design is in [the design record](../../design/next-steps.md#4-mcp-server-the-agreed-design-not-built)). diff --git a/docs/examples/Fibres/raman-soliton-shift/README.md b/docs/examples/Fibres/raman-soliton-shift/README.md index bb11f36..728b27a 100644 --- a/docs/examples/Fibres/raman-soliton-shift/README.md +++ b/docs/examples/Fibres/raman-soliton-shift/README.md @@ -23,18 +23,22 @@ python make_figures.py # optional: hero.png, animation.gif (about 40 s) ``` fundamental solitons N = 1, beta2 = -20 ps^2/km, gamma = 1.3 /(W km), 6 m of silica fibre, Raman response of the engine (T_R = 1.46 fs from the model) -T0 = 40 fs: shift after 6 m -7.9 THz (63 nm), rate -1.319 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.969 THz/m (ratio 1.36) -T0 = 50 fs: shift after 6 m -3.0 THz (24 nm), rate -0.503 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.397 THz/m (ratio 1.27) -T0 = 65 fs: shift after 6 m -1.0 THz (8 nm), rate -0.162 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.139 THz/m (ratio 1.16) +T0 = 40 fs: shift after 6 m -7.9 THz (63 nm), rate -1.319 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.969 THz/m (ratio 1.36); exact Raman gain -1.344 THz/m (ratio 0.981) +T0 = 50 fs: shift after 6 m -3.0 THz (24 nm), rate -0.503 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.397 THz/m (ratio 1.27); exact Raman gain -0.499 THz/m (ratio 1.007) +T0 = 65 fs: shift after 6 m -1.0 THz (8 nm), rate -0.162 THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = -0.139 THz/m (ratio 1.16); exact Raman gain -0.160 THz/m (ratio 1.013) log-log slope of the rate against T0: -4.32 (Gordon: -4) checks passed ``` -**What it shows.** The log-log slope of the shift rate against T₀ is -4.3 (Gordon: -4), over a factor 1.6 in T₀ (16 in rate): the -scaling is reproduced. The absolute rate is 16-36 % *above* Gordon's formula with the model's T_R. Possible reasons, not separated here: Gordon's formula assumes a soliton of constant width and a -single T_R, while the engine's pulse changes as it shifts and its Raman gain is not a single first moment. The example states the -disagreement rather than tuning T_R to remove it. The peak power of the soliton falls and the soliton slows down -(the group delay changes with the carrier) as it moves red (middle top panel). +**What it shows.** The log-log slope of the shift rate against T₀ is -4.3 (Gordon: -4): the scaling is reproduced. The absolute rate is 16-36 % *above* +Gordon's formula with the model's T_R, and **the cause is Gordon's approximation, not the engine.** Gordon replaces the Raman response by its first moment +(`h_R(t) ≈ T_R δ'(t)`), which is valid only when the soliton spectrum is narrow against the Raman gain, whose peak is at 13 THz. A 40 fs soliton has a spectral width +of about 9 THz, so the gain it sees rises faster than linearly across its spectrum. With the engine's own response `h_R` and the same fixed-width soliton, the rate is +`d⟨Ω⟩/dz = -γ f_R (1/E) ∫ (h_R * P) P' dt` (`exact_rate` in `raman_shift.py`; it reduces to Gordon's formula for a `δ'` response). It gives -1.344, -0.499 and +-0.160 THz/m, and the engine's measured rates are within 2 % of those (0.98, 1.01, 1.01). The ratio to Gordon grows from 1.15 at 65 fs to 1.39 at 40 fs because the +first-moment approximation fails more as the pulse gets shorter. The same fibre in pyNLO (an independent split-step code, 16384 points over 20 ps, no +self-steepening) gives -1.319, -0.504 and -0.162 THz/m over 3-6 m, within 0.3 % of the engine (-1.319, -0.503, -0.162) (`test_raman_soliton_shift_matches_pynlo_and_the_exact_gain_rate`). The peak +power of the soliton falls and the soliton slows down (the group delay changes with the carrier) as it moves red (middle top panel). **Limits.** Self-steepening, loss and higher dispersion are off; a real fibre has all three. The shift rate is measured over the last two thirds of the fibre by a straight-line fit. diff --git a/docs/examples/Fibres/raman-soliton-shift/raman_shift.py b/docs/examples/Fibres/raman-soliton-shift/raman_shift.py index 6b2bb2a..deea383 100644 --- a/docs/examples/Fibres/raman-soliton-shift/raman_shift.py +++ b/docs/examples/Fibres/raman-soliton-shift/raman_shift.py @@ -21,6 +21,21 @@ def raman_first_moment(): return FR * np.trapezoid(t * h, t) / norm +def exact_rate(t0): + """Shift rate of a fixed-width sech soliton with the model's exact Raman response (no first-moment approximation). + + The nonlinear phase is phi_z = gamma V with V = (1 - f_R) P + f_R (h_R * P), so the mean frequency changes by + d/dz = -gamma f_R (1 / E) integral (h_R * P)(t) P'(t) dt (the instantaneous Kerr part integrates to zero). For a delta-prime response it + gives Gordon's -8 T_R |beta2| / (15 T0^4) exactly. Returns rad/s per m (negative: red shift).""" + dt = 0.25e-15 + t = np.arange(-4e-12, 4e-12, dt) + ts = np.arange(0, 2e-12, dt) + h = (TAU1 ** 2 + TAU2 ** 2) / (TAU1 * TAU2 ** 2) * np.exp(-ts / TAU2) * np.sin(ts / TAU1) + p = abs(B2) / (GAMMA * t0 ** 2) / np.cosh(t / t0) ** 2 + f = np.convolve(p, h * dt)[:len(p)] + return -GAMMA * FR * abs(np.trapezoid(f * np.gradient(p, dt), t)) / np.trapezoid(p, t) + + def centroid_frequency(r): w, s = r["output_omega_rad_s"], r["output_spectrum"] return float(np.sum(w * s) / np.sum(s)) @@ -48,7 +63,7 @@ def main(): r = run() tr = raman_first_moment() print("fundamental solitons N = 1, beta2 = %.0f ps^2/km, gamma = %.1f /(W km), %.0f m of silica fibre, Raman response of the engine (T_R = %.2f fs from the model)" % (B2 * 1e27, GAMMA * 1e3, LENGTH, tr * 1e15)) - rates = [] + rates, exacts = [], [] for t0 in T0S: # the rate is measured over the last two thirds of the fibre (the soliton has settled); Gordon's formula gives the constant rate at fixed T0 d = r[t0] @@ -56,11 +71,15 @@ def main(): rate = np.polyfit(d["z"][sel], d["shift"][sel], 1)[0] # rad/s per m (negative: red shift) gordon = -8 * tr * abs(B2) / (15 * t0 ** 4) rates.append(rate) - print("T0 = %2.0f fs: shift after %.0f m %.1f THz (%.0f nm), rate %.3f THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = %.3f THz/m (ratio %.2f)" - % (t0 * 1e15, LENGTH, d["shift"][-1] / (2 * np.pi) * 1e-12, -d["shift"][-1] / (2 * np.pi) * LAM ** 2 / C * 1e9, rate / (2 * np.pi) * 1e-12, gordon / (2 * np.pi) * 1e-12, rate / gordon)) + exact = exact_rate(t0) + exacts.append(exact) + print("T0 = %2.0f fs: shift after %.0f m %.1f THz (%.0f nm), rate %.3f THz/m; Gordon -8 T_R |beta2| / (15 T0^4) = %.3f THz/m (ratio %.2f); exact Raman gain %.3f THz/m (ratio %.3f)" + % (t0 * 1e15, LENGTH, d["shift"][-1] / (2 * np.pi) * 1e-12, -d["shift"][-1] / (2 * np.pi) * LAM ** 2 / C * 1e9, rate / (2 * np.pi) * 1e-12, gordon / (2 * np.pi) * 1e-12, rate / gordon, + exact / (2 * np.pi) * 1e-12, rate / exact)) slope = np.polyfit(np.log(T0S), np.log(-np.array(rates)), 1)[0] print("log-log slope of the rate against T0: %.2f (Gordon: -4)" % slope) assert abs(slope + 4) < 0.5 + assert all(abs(rr / ex - 1) < 0.05 for rr, ex in zip(rates, exacts)) # the engine against the exact-gain rate of a fixed-width soliton assert all(0.4 < rr / (-8 * tr * abs(B2) / (15 * t ** 4)) < 2.5 for rr, t in zip(rates, T0S)) print("checks passed") diff --git a/docs/examples/Nonlinear/filament/README.md b/docs/examples/Nonlinear/filament/README.md index 688fbcf..d346477 100644 --- a/docs/examples/Nonlinear/filament/README.md +++ b/docs/examples/Nonlinear/filament/README.md @@ -16,10 +16,10 @@ ultrafast retrieve pcgpa results/result.npz -o retrieved # FROG -> pul What to look at: -* `summary.json → elements[0].values`: `peakElectronDensityM3` (~2·10²² m⁻³) and `peakIonizationFraction` (~10⁻³). -* The spectrum broadens (B ≈ 7 rad) and its centroid moves to the red (814 nm): the delayed rotational response +* `summary.json → elements[0].values`: `peakElectronDensityM3` (~1.9·10²² m⁻³) and `peakIonizationFraction` (~7·10⁻⁴). +* The spectrum broadens (B ≈ 6.6 rad) and its centroid moves to the red (815 nm): the delayed rotational response red-shifts the trailing part of the pulse more than the weak plasma blue-shifts the leading part. -* The FROG and its retrieval: `retrieved/retrieval.json` (G ≈ 4·10⁻⁵, FWHM 47.6 fs vs 48.3 fs simulated); +* The FROG and its retrieval: `retrieved/retrieval.json` (G ≈ 10⁻⁵, FWHM 50.3 fs vs 51.3 fs simulated); `retrieved/retrieved_spectrum.csv` can be the input of another run (`"pulse": {"shape": "spectrum", "spectrum": {"csv": "retrieved/retrieved_spectrum.csv"}, "energyJ": 3.7e-4}`). @@ -27,4 +27,4 @@ What to look at: modelled. For a real filament give the element `"transverse": {"model": "radial", "points": 128, "radiusM": 1e-3, "equation": "uppe"}` (much slower: minutes). -Try: `"rotationalRaman": false` on a custom air material, or `"ionization": false`, and compare the spectra. +Try: `"rotationalRaman": false` on a custom air material, or `"ionization": false`, and compare the spectra. `"ionization": {"frequencyResolved": false}` applies the Drude index at the carrier only, as versions before this one did: the centroid moves to 813 nm and the energy loss drops from 8.8 % to 7.5 %, because the electrons born in the field also take their drift energy U_p in the frequency-resolved model ([Ionisation](../../../reference/ionization.md#plasma)). diff --git a/docs/plugins/ao.md b/docs/plugins/ao.md index 9061ebb..538c34e 100644 --- a/docs/plugins/ao.md +++ b/docs/plugins/ao.md @@ -75,6 +75,14 @@ wavelength it is scaled by the liquid crystal's dispersion. | `crosstalkSigmaCells`, `crosstalkKernel` | pixels | 0 | fringing field | | `zeroOrderLeak` | | 0 | amplitude that passes unmodulated | +**Placement on the engine's grid.** The simulation grid has a sample on the axis (`x = (i - nx/2) dx`); an even panel centred on the axis has a pixel *edge* there. With a +pixel pitch equal to the grid step and `offsetXM = offsetYM = 0`, every grid cell therefore lies half on one pixel and half on the next and takes their average. A blaze of P pixels per +period then keeps only `cos^2(pi / P)` of its light (0.854 at P = 8, 0.962 at P = 16), and a Fourier prediction of the written phase is off by the same amount. Set `offsetXM = offsetYM = +-pitchM / 2` to put the pixel centres on the grid points: the same blaze then keeps all its light (1.0000, `test_slm_pixels_on_the_grid_points_...`) and the focal field equals an exact +angular-spectrum propagation of the sampled field to 1 % (`docs/examples/AdaptiveOptics/slm-beam-shaping`). The engine warns ("averages two device cells") when the pitch equals the grid step +and the cell edges fall on grid points. With a grid step of half the pitch or less the average over a pixel is the physical pixel structure and no shift is needed. The engine's absorbing edge removes the replica orders +that a periodic grid would wrap round the window: compare with a periodic reference only inside the window, or switch the absorbing edge off for the comparison. + **Chromatic model.** The 2 pi reset written at the design wavelength `lambda_d` is `2 pi s(lambda)` at `lambda`, with `s = lambda_d dn(lambda) / (lambda dn(lambda_d))`. A blazed ramp of period `n` pixels then sends the fraction `sinc^2(1 - s)` into the first order (and the rest into the zero and second orders). `dispersion: "none"` gives `s = lambda_d / lambda`. The pixel phases go to @@ -103,7 +111,7 @@ A Fourier-plane mask of a 4f line. Frequency maps to position `x = D (2 pi c / o **The programmed delay stays in the transfer function**: a linear phase `tau (omega - omega_0)` moves the pulse by `tau` inside the time window; only the carrier phase is taken out of the frame, so the arrival-time bookkeeping is unchanged. A pixelated mask makes side -pulses at `2 pi / (pixel bandwidth)`: **set `grid.windowS` to cover them**, the grid planner does not know about them (nor about a large programmed GDD, which stretches the pulse: nothing warns when the output wraps round the window). The spectral grid spacing is `2 pi / windowS`: keep at least ~10 samples per pixel, otherwise the sampled mask edges change the harmonics (a window of 8 ps with 1.6 ps side pulses, 5 samples per pixel, gave 2.5 % errors in the side pulses). With gaps (active +pulses at `2 pi / (pixel bandwidth)`: the grid planner makes room for them (orders 1 to 8, each weighted by the spot-size factor `exp(-2 (pi m w0 / pitch)^2)`; a mask with no steps and no gaps adds none). It also stretches the window for a programmed `aopdf` GDD by `|GDD| x input bandwidth`, and for the `aopdf` delay and replica delay. With `grid.auto: false` and a window that is too small it **warns** that the output will wrap. The planner knows the programmed GDD of an `aopdf` only, not a quadratic phase written into the pixels of a `spectralShaper`: for that, set `grid.windowS`. The spectral grid spacing is `2 pi / windowS`: keep at least ~10 samples per pixel, otherwise the sampled mask edges change the harmonics (a window of 8 ps with 1.6 ps side pulses, 5 samples per pixel, gave 2.5 % errors in the side pulses). With gaps (active fraction `f`) the side pulses carry `[sin(pi f)/(pi f)]^2` of the main pulse (Weiner, Rev. Sci. Instrum. 71, 1929 (2000)). ## aopdf @@ -139,7 +147,7 @@ of the aligned value (derivation: the overlap of two Gaussian envelopes shifted | `sensorless` | `spgd`, `hill_climb`, `modal_search`, `transmission_matrix`, `focus_phases` | Vorontsov and Sivokon, JOSA A 15, 2745 (1998); Vellekoop and Mosk, Opt. Lett. 32, 2309 (2007) | Things measured while writing them (and stated in their tests): quantisation in the GS loop helps at 2 and 3 grey levels but **not** at 4 -or 8 on the test target; SPGD with `gain 20, amplitude 0.2` on 16 channels diverges (gain 10 locks to 1.000 on 10 of 10 seeds); +or 8 on the test target; SPGD with the classical fixed gain 20 and `amplitude 0.2` on 16 channels diverges (gain 10 locks to 1.000 on 10 of 10 seeds), and a fixed gain that works at metric scale 1 does nothing at 1e-3 or 1e3 (see [SPGD gain](#spgd-gain)); modal search climbs by one `bias` per sweep until the metric is concave, because a peak-type metric is convex far from the optimum; the phase-stepping transmission matrix recovers the phases exactly (mixed second difference: no reference arm, any output however dark) but the amplitudes only relative within a row. @@ -155,15 +163,48 @@ dark) but the amplitudes only relative within a row. | SLM at other wavelengths (`AoDeviceTests`) | `s(lambda)` from the Cauchy law; the first-order coefficient with phases scaled by `s` | | phase target to the nearest level of a tanh curve (`AoDeviceTests`) | identical fields to 1e-12 | | 1920 x 1080, 256 levels (`AoDeviceTests`, slow) | S2 scale budget; erf product of the overfilling Gaussian | +| grid-planner room for shaper side pulses and programmed GDD, warning on a fixed window (`AoDeviceTests.Planner_*`) | Weiner side-pulse delay `2 pi / d_omega`; Gaussian chirp formula; window >= 2 x the stretched pulse | +| shaper grating dispersion at Littrow (`AoDeviceTests.SpectralShaper_GratingDispersion_*`) | `dx/dlambda = 2 f tan(theta) / lambda` | | aopdf delay, efficiency, GDD, window, replica (`AoDeviceTests`) | shift theorem; Diels and Rudolph chirp formula; erf of the cut spectrum | | spectral shaper side pulses and delay (`AoDeviceTests`) | Weiner; shift theorem | | Strehl, Maréchal; twin vs engine; fit residual vs actuators (`test_ao_plugin.py`) | exact on-axis field `||^2`; `exp(-sigma^2)` | | one-shot correction, closed loop, modal search (`test_ao_plugin.py`) | twin prediction; `(1 - g)^k` | | tiled array N, pistons, delays, SPGD lock (`test_ao_plugin.py`) | `|sum e^{i phi}|^2 / N^2`; Gaussian envelope overlap | +| Bolduc and double-phase maps through the `slm` element with a varying complex field (`test_ao_plugin.py`) | first order / low-frequency field = Fourier transform of illumination x `A e^{i phi}` (correlation > 0.99; energy against the same ratio of transforms, 3 %); the Bolduc energy is taken relative to an engine blaze of A = 1 because of the SLM efficiency finding: an 8-pixel blaze on a grid whose cells straddle the pixels keeps 0.85 of the light (explained and fixed by the half-pixel offset, see Placement on the engine's grid; this test keeps the ratio so that it needs no offset) | +| SPGD default step: scale invariance (1e-9) and at least 0.9 of the optimum at metric scales 1e-3 .. 1e3, engine deformable-mirror focus (`test_ao_algorithms.py`, `test_ao_plugin.py`) | exact invariance; measured worst 0.98, 0.996, 0.994 | +| SLM pixels on the grid points: a blaze keeps `cos^2(pi/P)` of its light half a pixel off the grid, all of it (1e-6) with the offset; the warning (`test_ao_plugin.py`) | exact sampled-field arithmetic | +| tiled array: identical tiles reach the axis alike only when the lens is resolved (`test_ao_plugin.py`) | equal tiles, equal axis fluence (1e-3); unequal by 56 when the lens phase exceeds pi per cell | +| tiled-array central and grating lobe fractions, in phase and with checkerboard pistons (`test_ao_plugin.py`) | array factor x Gaussian tile envelope on the engine's grid, 1e-3 (measured 2e-5) | | GS monotone, WGS uniformity, Bolduc first order, double phase (`test_ao_algorithms.py`) | Fienup 1982; Di Leonardo 2007; Bolduc 2013 | | Shack-Hartmann spot shift `f dW/dx`; modal fit exact; Southwell exact for quadratics and second-order convergent (`test_ao_algorithms.py`) | geometric optics; analytic | | transmission matrix and focusing enhancement `(pi/4)(N - 1) + 1` (`test_ao_algorithms.py`) | Vellekoop and Mosk 2007 (3000 outputs, tolerance 0.6 %) | +## SPGD gain + +`sensorless.spgd(metric, x0, amplitude=...)` is **scale-free by default**: the update is `x += step (dJ / S) d`, where `d` is the random +-`amplitude` shake of all +channels, `dJ = J(x + d) - J(x - d)` and `S` the largest `|dJ|` of the last ~33 iterations (`memory=0.97`). A channel therefore never moves by more than +`step x amplitude` per iteration, the step shrinks by itself as the metric flattens, and multiplying the metric by any positive number gives the same iterates +(a test checks this to 1e-9). Pass `gain=` for the classical update `x += gain dJ d`, whose gain is in units of 1 / metric: it worked only in one range of +metric scales and had to be found by trial (for the deformable-mirror example, gain 1.5 reached Strehl 0.64, 3 reached 0.95, 5 reached 0.80). + +Measured (10 seeds, worst final value over the optimum; `amplitude` 0.2 rad for locking, 0.05 for the Strehl peak): + +| `step` | 16-channel lock | 7-channel lock | 6-mode Strehl peak | +|---|---|---|---| +| 0.1 | 0.37 | 0.73 | 0.95 | +| 0.2 | 0.83 | 1.00 | 1.00 | +| **0.3** | 0.99 | 0.99 | 1.00 | +| **0.4 (default)** | 0.98 | 1.00 | 0.99 | +| **0.6** | 0.97 | 0.99 | 0.99 | +| **1.0** | 0.94 | 0.96 | 0.97 | +| 2.0 | 0.70 | 0.85 | 0.88 | + +At the default, the worst value is 0.982, 0.996 and 0.994 for metric scales 1e-3, 1 and 1e3 alike. The engine's deformable-mirror bench (six hidden aberration +modes, camera peak as the only measurement, 80 iterations) reaches at least 0.9 on three seeds. How to choose: `amplitude` about a tenth of the length over which +the metric changes by a factor of 2 (too large and the metric is not locally linear, too small and noise in a measured metric dominates), `step` between 0.3 and +1; if the loop oscillates at the end, lower `step`; if the metric is noisy, raise `memory` towards 0.99 so that one lucky difference does not set the scale for long. Gain sequences that fall with the +iteration number (Spall) are not implemented; the decaying maximum does the same job without a schedule. + ## Limits * Devices are idealised unless you supply measured curves. No hysteresis, creep, thermal drift, polarisation dependence or SLM diff --git a/docs/plugins/twin.md b/docs/plugins/twin.md index 2793dc6..f742b39 100644 --- a/docs/plugins/twin.md +++ b/docs/plugins/twin.md @@ -95,7 +95,7 @@ construction of the synthetic lab (placement; a phase proportional to the energy ## Limits and not built * The MCP server for agents (T6: eight workspace-scoped tools, user expressions refused, time and memory budgets) is designed - (`docs/design/notes/s4-twin.md`, section 5) and **not built**. + ([design record](../design/next-steps.md#4-mcp-server-the-agreed-design-not-built)) and **not built**. * The only project template is the synthetic spectral lab; a spatial (wavefront) template does not exist. * Gradients through the engine are finite differences (one run per parameter and condition); the network transform needed 1050 runs in its test. diff --git a/docs/reference/branches.md b/docs/reference/branches.md index dd6ad80..db55ae2 100644 --- a/docs/reference/branches.md +++ b/docs/reference/branches.md @@ -1,6 +1,6 @@ # Branched systems -A system is `Sequential` (the default: a list, each element continuing into the next) or `Branched` (each element states where its light goes). Both compile to the same graph and run through the same element code. Design: [Branched systems](../design/branching.md). +A system is `Sequential` (the default: a list, each element continuing into the next) or `Branched` (each element states where its light goes). Both compile to the same graph and run through the same element code. Design: [Branched systems](../reference/branches.md). ## Writing one diff --git a/docs/reference/components.md b/docs/reference/components.md index 87286db..4593f67 100644 --- a/docs/reference/components.md +++ b/docs/reference/components.md @@ -50,8 +50,8 @@ Each element reports `trackedBeamRadiusXM`, `trackedBeamRadiusYM` and the wavefr tracking carries on unchanged. Simple astigmatism only: the principal axes are x and y. A fold plane at an arbitrary angle is snapped to the nearer -axis (with a note). The thermal lens of a gain element is included. A multipass amplifier puts its thermal lens on the tracked beam -only when given the path between passes (`thermalLens.track`, `passGapM`); a regenerative amplifier does not +axis (with a note). The thermal lens of a gain element is included. The lens is a graded-index duct per slice (exact ABCD, not a thin lens). A multipass amplifier puts its thermal lens on the tracked beam +only when given the path between passes (`thermalLens.track`, `passGapM`); a regenerative amplifier takes it the same way with an integer `roundTrips` ([Gain media](gain.md#thermal-lens)). ## Spatio-temporal couplings diff --git a/docs/reference/crystals.md b/docs/reference/crystals.md index 1b4275d..64cb94d 100644 --- a/docs/reference/crystals.md +++ b/docs/reference/crystals.md @@ -39,6 +39,8 @@ and the idler is ω_p − ω_s, whichever is higher in frequency. ## Built-in crystals +The indices, d tensors and n2 of the built-in crystals are catalogue files (`Data/materials/crystals`), each quantity with its source and status; see [the material catalogue](materials.md#the-material-catalogue) and the generated list [Material catalogue](materials-catalogue.md#chi2-crystals). The index sets of BBO, KTP, KDP, LiNbO3 (o-ray), BiBO and AgGaSe2 are verified against the refractiveindex.info pages; LBO (Kato 1994), the temperature-dependent LiNbO3 / MgO:LiNbO3 e-ray forms, the d tensors and the n2 values are not. A crystal element that uses an unverified value says so in the run notes. + | name | class / point group | indices | d (pm/V) | |---|---|---|---| | `BBO` | uniaxial (negative), 3m | Tamosauskas et al. 2018 | d22 = 2.2, d31 = 0.04 | diff --git a/docs/reference/gain.md b/docs/reference/gain.md index 81ff259..6cc553e 100644 --- a/docs/reference/gain.md +++ b/docs/reference/gain.md @@ -20,7 +20,7 @@ elements: `gain` (one pass through a pumped crystal, 1-D or spatial) and `amplif | `Ti:sapphire` | 3.2 µs | 3.3e25 m⁻³ | ≈ 795 nm, σe ≈ 3e-19 cm² | four-level, broadband, pumped at 490–532 nm | | `Nd:YAG` | 230 µs | 1 at.% | 1064.2 nm, σe ≈ 2.8e-19 cm² | four-level, narrow line, pumped at 808 nm | -The cross sections are smooth fits to the published spectra (references in `provenance.json`), good to roughly +The numbers of these media (lifetime, doping, bands, thermal and stress constants) are catalogue files (`Data/materials/gain`), each with its source and status (all are `approximate` or `unverified` so far; see [Material catalogue](materials-catalogue.md#gain-media)). A run that uses one says so in its notes and in `summary.json` (`unverifiedInputs`); thermal data are reported only with a thermal lens, stress constants only with `stress`. `physics.strictMaterials` refuses such a run ([strict mode](materials.md#strict-mode)). The cross sections are smooth fits to the published spectra (references in `provenance.json`), good to roughly 10–20 %; replace them with your own data when it matters. For quasi-three-level media the emission and absorption spectra are linked by the **McCumber relation** σe/σa = exp((E_zl − hν)/kT), so each one also has the other's reabsorption tail. At room temperature this makes an unpumped Yb:YAG crystal absorb at 1030 nm (see example 15). @@ -97,11 +97,48 @@ off with `"steadyState": false`, or tune it with `{"maxPulses": 400, "tolerance" ## ASE -`"ase": true` (with `aseSeed`) on a 1-D gain or amplifier element seeds amplified spontaneous emission. In every -slice, each frequency bin (one temporal mode) receives noise of mean energy ħω·σe(ω)·N2·dz in the signal's -spatial mode and polarisation. This is spontaneous emission into the mode, which is then amplified like the -signal. The seeded energy is booked as `ASE (spontaneous emission)` (negative) and reported as -`aseSeedEnergyJ`. Only the time window is simulated, so the ASE emitted between pulses is not counted. +Two models, which can be used together. + +**Noise in the pulse window** (`"ase": true`, `aseSeed`; 1-D only). In every slice each frequency bin (one temporal mode) receives noise of mean energy +ħω·σe(ω)·N2·dz in the signal's spatial mode and polarisation, which is then amplified like the signal. The seeded energy is booked as +`ASE (spontaneous emission)` (negative) and reported as `aseSeedEnergyJ`. Only the time window is simulated. + +**ASE over the inversion lifetime** (`"aseModel": true` or an object; gain and amplifier elements). Between pulses (milliseconds) the ASE crosses the crystal in +picoseconds, so it is solved at frozen N2 and then depletes N2 in the rate equations (`PumpCw`, the decay between pulses, the steady state of the pulse train, a single shot). +For every transverse cell and every frequency ν of the emission band, the intensity I (W/m²/Hz) of the forward (from z = 0) and backward (from z = L) ASE obeys + +`dI/dz = ±γ I + p h ν σe N2 q`, `γ = σe N2 − σa (N_t − N2)`, `q = n² Ω / λ0²` + +with p the number of polarisations (default 2) and Ω the solid angle of the rays that matter. The slice step is exact for constant γ. The stimulated photons of each slice +(output − input − source) are taken from N2, so the photon balance is exact; the spontaneous part is already in the lifetime 1/τ and is not subtracted twice. Keys of `aseModel`: + +| key | meaning | default | +|---|---|---| +| `solidAngleSr` | effective solid angle inside the medium of the ASE that depletes the inversion | min(π r_pump² / L², escape cone 2π(1 − √(1 − 1/n²))) | +| `polarizations` | 1 or 2 | 2 | +| `points` | frequencies over the emission band (where σe > 10⁻³ of its maximum) | 40 | +| `bandLowerM`, `bandUpperM` | restrict the band | automatic | + +What it reports (values of the element; they are *not* in the pulse's energy ledger, which stays closed): `aseForwardPowerW`, `aseBackwardPowerW` (ASE leaving the signal +mode, one spatial mode of area π w²/2, at the state the next pulse sees), the spectra `aseForwardSpectrumWPerHz`, `aseBackwardSpectrumWPerHz` on `aseWavelengthM`, +`aseStimulatedPowerW` (the power the ASE takes from the inversion), `aseDepletionFraction` (that over the absorbed pump power), `aseSolidAngleSr`, and, with a repetition rate, +`asePedestalEnergyJ` and `asePedestalEnergyFraction`: the ASE energy in the signal mode per pulse period relative to the output pulse. That fraction is the strength to +give a `pulse.pedestal` of kind `ase` in the next stage (`energyFraction`). + +Checks (`AseStressTests`): uniform inversion gives n_sp h ν Δν (G − 1) per polarisation at both ends (1e-12; Desurvire, *Erbium-doped fiber amplifiers*, 1994; the factor 2 for both +polarisations); a non-uniform inversion against a direct RK4 integration (1e-6); the source into 4π equals N2 ⟨hν⟩ / τ_rad by the Füchtbauer-Ladenburg relation (1e-9; fixes the +etendue factor q); the slice step integrates the homogeneously saturated law ln(G/G0) + (G − 1) P_in/P_sat = 0 (1e-4); in the cw steady state absorbed pump photons = N2 V / τ + stimulated +ASE photons (0.5 %); no solid angle gives no depletion and more angle gives more. + +**Limits (stated, not hidden).** +* The depletion uses one *effective* solid angle for all rays. Real ASE in a bulk crystal comes from all directions and the exact depletion depends on the geometry: the default is a + physical estimate (rays that stay in the pump region over the length L, capped by the escape cone), not a ray trace. Give `solidAngleSr` when the geometry is known. For a fibre use + Ω = λ0² / (n² A) (one mode). +* **Transversely resolved ("3-D") ASE is not in this package.** On a transverse grid (`transverse`) every cell is its own column with its own inversion, so the depletion is local and follows the + pump; but the rays do not cross from cell to cell, and there are no reflections at the faces (no parasitic lasing, no transverse ASE in a large-aperture disk). A true 3-D model needs + oblique rays through the whole crystal (a Monte Carlo or ray-tiling model, O(cells² × slices)) and a reference for it (see `docs/design/next-steps.md`). +* The signal polarisation is not special: the same ASE depletes the inversion for both polarisations. ASE in the *pulse's* polarisation state is the input of the pedestal. +* Cost: cells × slices × points exponentials per evaluation, once per pumping step. A 1-D element costs milliseconds; a 64 × 64 cell grid costs seconds. ## Thermal lens @@ -122,87 +159,59 @@ Check: top-hat pump f = 2πKw²/(P_h·dn/dT); Gaussian pump f = πKw²/(P_h·dn/ **On the tracked beam.** Elements after a gain element take its thermal lens through the tracked q parameters. The tracking happens when the system is compiled, before the pulse train has run, so the engine evaluates the same heat -model with the pump alone (cw pumping to equilibrium, or a pulsed pump repeated to steady state at the repetition rate) -and applies one thin lens per slice. Each slice's lens is the parabola fitted to its Δn(r) **with the beam's intensity as -weight** (the lens of least rms wavefront error over the beam), using the tracked beam radius at the element (or -`modeRadiusM` / `beam`). For a beam much smaller than the pump this is the on-axis lens; for a beam that fills the pumped -region it is weaker, since the Δn of a Gaussian pump flattens off the axis. The element reports `trackedThermalLensFocalLengthM` -(the sum of the slices) and a note with the on-axis value. If the run's own thermal lens, with the signal extracting energy, -differs from the pump-alone estimate by more than 5 %, a note says so. A measured value replaces the model: +model with the pump alone (cw pumping to equilibrium, or a pulsed pump repeated to steady state at the repetition rate). +The pumped region is a **distributed lens**: each slice of the medium is a graded-index duct, n(r) = n₀ (1 − g² r²/2), +with the exact reduced ABCD matrix A = D = cos gL, B = sin(gL)/(n₀ g), C = −n₀ g sin gL (g² < 0 gives cosh / sinh for a +defocusing host), not a thin lens at one end. The power of each slice comes from the parabola fitted to its Δn(r) +**with the beam's intensity as weight** (the lens of least rms wavefront error over the beam), using the tracked beam radius +at the element (or `modeRadiusM` / `beam`): g² = (1/f_j)/(n₀ Δz). For a beam much smaller than the pump this is the on-axis +lens; for a beam that fills the pumped region it is weaker, since the Δn of a Gaussian pump flattens off the axis. The +change against slices of thin lenses is of order (gL)²: 0.4 % for the 4 mm rod of the examples, and it matters for long +media (gL = 0.6 changes the beam radius behind a 10 cm rod by 1 %). + +The element reports two focal lengths, both with the unit in the key: `trackedThermalLensFocalLengthM`, the +**thin-lens equivalent** 1/f = n₀ g² L summed over the slices, and `trackedThermalLensEffectiveFocalLengthM`, the +**effective** focal length −1/C of the whole duct, which is what a measurement from outside the medium gives +(f_eff = f sin(gL)/(gL) for a uniform duct). A note gives the on-axis value of the pump alone. If the run's own thermal lens, +with the signal extracting energy, differs from the pump-alone estimate by more than 5 %, a note says so. A measured value +replaces the model and is the **effective** focal length: the engine finds the uniform duct that has it +(a focal length shorter than a quarter pitch of the medium can give, gL = π/2, is capped with a note): ```jsonc -"thermalLens": { "focalLengthM": 0.45 } // one thin lens at the centre: tracking and the 1-D thin lens +"thermalLens": { "focalLengthM": 0.45 } // measured effective focal length: tracking (a uniform duct) and the 1-D thin lens "thermalLens": { "rodRadiusM": 2e-3, "track": false } // model, but leave the tracked beam alone ``` In a **multipass amplifier** the tracked beam takes the lens only on request, since the path between passes is otherwise unknown: `"thermalLens": {"track": true, "passGapM": 0.2}` -applies the lens once per pass with that free path between them. Regenerative amplifiers do not put it on the tracked beam. +applies the lens once per pass with that free path between them. A **regenerative amplifier** takes it the same way, with an +integer `roundTrips` (`"auto"` is decided at run time, after the beam is tracked, so it is not tracked and a note says so): +`roundTrips x passesPerRoundTrip` passes through the duct, `passGapM` between them. The path is open: the cavity's own focusing optics are not part of the tracked beam, so +for a stable resonator give the gap of the equivalent unfolded path or use the resonator element. Both amplifier kinds +report the two focal lengths above. + +What the Gaussian tracker leaves out is the aberrated part of the lens (the spherical aberration of the non-parabolic heat profile) +and gain narrowing; both need the radial spatial model (`transverse`), which has the full Δn(r, z). Against it, a 0.5 mm beam in a 60 W rod +pumped by a 0.6 mm Gaussian: the radius 0.4 m behind the rod differs by 5 % (the lens is aberrated, M² = 1.08), the radius at the exit of the rod by 9 % (gain +narrowing, which the tracker does not model); a 0.2 mm beam, which probes the aberrated centre of the lens, by 23 %. **Stress birefringence.** `"thermalLens": {"stress": true}` adds the thermal birefringence of a uniformly heated rod of a cubic host (Koechner, *Solid-State Laser Engineering*, ch. 7; built in for YAG hosts, `thermalExpansionPerK`, `stressCr`, `stressCphi` on a medium of your own). The radial and tangential polarisations differ by the retardation δ = C (r/r₀)², C = n₀³ α C_B P_h / (λ K), C_B = C_r − C_φ. The depolarised fraction is the average of sin²(2φ) sin²(δ/2) over a Gaussian beam of radius w inside the rod. A beam filling the rod gives Koechner's D = (1 − sin C / C) / 4, tending to 1/4; a beam well inside it gives D = x² / (1 + x²) / 4 with x = C w² / (2 r₀²). The gain element rotates -that fraction of the field between x and y, which keeps the energy (the beam-averaged result, not the four-lobed pattern) and reports `depolarizationFraction`, `stressRetardationRad`, and the two lenses +that fraction of the field between x and y, which keeps the energy (the beam-averaged result) and reports `depolarizationFraction`, `stressRetardationRad`, and the two lenses `stressFocalLengthRadialM`, `stressFocalLengthTangentialM` (dn/dT, stress and end-face bulging). The formula assumes uniform heating over the rod: an end-pumped rod is only roughly described. -Checks (`BeamTrackingTests`): a small beam sees Innocenzi's f = πKw_p²/(P_h·dn/dT) (Appl. Phys. Lett. 56, 1831 (1990); 5 %); a -measured focal length focuses the tracked beam as a thin lens (waist radius 0.2 %); for a 0.5 mm beam in a rod pumped by a 0.6 mm +*The four-lobed pattern.* On a **cartesian** transverse grid (`ny > 1`) with an x-polarised input the gain element applies, at every point (x, y), the Jones matrix of a +rod whose eigen-polarisations are the radial and the tangential ones: R(−φ) diag(1, e^{iδ}) R(φ), δ = C min(r, r₀)² / r₀² (the tangential one is delayed for C > 0). An x-polarised beam then leaks +|E|² sin²(2φ) sin²(δ/2) into y: no light on the axes, four lobes on the diagonals with alternating sign. The profile the beam carries afterwards has its own y shape (`DataY`), so a following +spatial element propagates the vector field. The matrix is unitary (energy kept to 1e-13) and the beam average equals the lumped fraction `D = (1 − ⟨cos δ⟩)/4` to 1e-6 on the grid (test). The element +reports `depolarizationFractionPattern` beside `depolarizationFraction`; the pattern sees the beam at the exit of the rod, the lumped value the beam at the middle, so they differ by the change of the +radius in the thermal lens (D grows as w⁴ for a small beam). Limits: the radial grid (no 2-D map; the lumped rotation is used), a y component at the input (the spatial gain is scalar; a note says so and the lumped +form is used), and the common part of the radial and tangential lenses (`stressFocalLength*`) is reported but not applied to the field; only the polarisation-dependent phase difference is. +The pattern is applied once at the exit, after the gain, and ignores the growth of the retardation along z inside the rod. + +Checks (`BeamTrackingTests`): a small beam sees Innocenzi's f = πKw_p²/(P_h·dn/dT) (Appl. Phys. Lett. 56, 1831 (1990); 5 %); the duct matrix is +cos(gL), sin(gL)/(n₀g), −n₀g sin(gL) (1e-12), with the thin-lens limit and the hyperbolic case; N thin slices converge to the duct as 1/N²; +a measured effective focal length focuses the tracked beam like the exact duct (1e-9) and the thin lens at the centre of a 10 cm rod would be 1 % off; a +measured focal length focuses a thin rod like a thin lens (waist radius 0.2 %); N round trips of a regenerative amplifier equal N x passesPerRoundTrip passes of a multipass amplifier (1e-12); for a 0.5 mm beam in a rod pumped by a 0.6 mm Gaussian, the tracked radius 0.4 m behind the rod is within 7 % of the radial spatial model (which also shows M² = 1.08 from the lens's aberration, beyond a Gaussian beam parameter). - -The element also records the lens as a Gradient-1 GRIN (z-averaged): `thermalGrinNr2PerM2`, -`thermalDnOnAxis`, `hostIndex`. `UltrafastEngine.Zemax thermal-lens` writes it into OpticStudio -([Zemax plugin](../plugins/zemax.md)). - -**Repetition rate.** An element `repRateHz` overrides the pulse's (a pulse picker in front of the amplifier); the -output pulse carries the element's rate. - -## The amplifier element - -```jsonc -{ "id": "mp", "type": "amplifier", "geometry": "multipass", "passes": 8, "medium": "Yb:YAG", "lengthM": 0.006, - "modeRadiusM": 0.45e-3, "pump": { ... }, - "betweenPasses": { "loss": 0.03, "gddFs2": 0, "todFs3": 0, "materials": [ { "name": "FusedSilica", "lengthM": 0.003 } ] } } - -{ "id": "rg", "type": "amplifier", "geometry": "regenerative", "roundTrips": "auto", // or a number - "passesPerRoundTrip": 2, "maxRoundTrips": 300, "medium": "Ti:sapphire", "lengthM": 0.012, "pump": { ... }, - "cavity": { "lossPerRoundTrip": 0.06, "gddFs2": 0, "materials": [ ... ] } } -``` - -Passes alternate direction through the crystal. The materials between passes add their dispersion and SPM (on the -mode area). With `"roundTrips": "auto"` the pulse is ejected at the energy maximum. The intracavity energy history -is kept so you can see the build-up. The amplifier is 1-D; use a chain of `gain` elements and `gap`s for a -spatially resolved multipass. - -## Results - -Values (`summary.json → elements[i].values`): `smallSignalGain`, `gainPeakWavelengthM`, `storedEnergyJ` -(above transparency at the carrier), `extractedEnergyJ`, `extractionEfficiency`, `inversionFractionBefore/After` -(on axis), `saturationFluenceJPerM2`, `absorbedPumpEnergyJ`, `absorbedPumpPowerW`, `heatW`, -`thermalLensFocalLengthM`, `peakTemperatureRiseK`, `gain`, `averageOutputPowerW`, `segments`, `steadyStatePulses`, -`converged`, `periodDoubling`, `passes` / `roundTrips`. - -Arrays (`result.npz`, `el__`): `gainSpectrumWavelengthM` + `gainSpectrum`, `zM` + `inversionBefore` / -`inversionAfter`, `energyVsPulse` (the train), `energyPerPass` / `energyPerRoundTrip`, and for spatial gain -`cellXM`, `cellYM`, `inversionVsCellBefore/After`. `ultrafast-plot` draws them in `gain.png`. - -## Validation - -`GainTests` checks: - -* Frantz–Nodvik saturated gain, to 3e-4; -* the small-signal gain specification; -* the cw rate-equation steady state; -* top-hat and Gaussian thermal-lens focal lengths; -* regen small-signal growth (G0²(1 − L))^N; -* auto ejection at the energy maximum, with the energy budget closing; -* the red shift of a red-leading chirp (time ordering); -* the pulse train against the full model; -* spatial gain with the thermal lens. - -## Limitations - -* Gain is scalar and polarisation independent; there is no spectral hole burning or excited-state absorption. - ASE is 1-D only and in the signal polarisation. -* The thermal lens is radial (rod geometry) and has no end-face bulging. Stress birefringence (`thermalLens.stress`) is the beam-averaged depolarisation of a uniformly heated rod of a cubic host, not the four-lobed pattern; end-face bulging enters only the two reported `stressFocalLength…M` values. -* Steady-state spatial trains use the input fluence profile for all pulses (no diffraction in the energy model). -* Cross sections are approximate (see above). diff --git a/docs/reference/ionization.md b/docs/reference/ionization.md index f59914f..a423cf4 100644 --- a/docs/reference/ionization.md +++ b/docs/reference/ionization.md @@ -29,17 +29,37 @@ The electron density follows, along the pulse at every point (1-D: the on-axis i dρ/dt = Σ_s W_s(I) (ρ_s - ρ_s,ionised) + (σ I / U) ρ - ρ/τ_rec - β ρ², -and acts as +and acts as a free-electron (Drude) index, frequency by frequency. At the carrier Δn = -(ρ / 2 n₀ ρ_c) (1 - i/(ω₀ τ_c)) / (1 + 1/(ω₀ τ_c)²), ρ_c = ε₀ m_e ω₀² / e², -plus the ionisation loss Im Δn += U W ρ_n / (2 k₀ I). σ = k₀ ω₀ τ_c / (n₀ ρ_c (1 + ω₀² τ_c²)) is the avalanche cross -section (on by default for solids). Defaults: gases τ_c = 350 fs / p[bar], no recombination; solids τ_c = 1 fs, -τ_rec = 150 fs. Every element reports `peakElectronDensityM3` and `peakIonizationFraction`. +and across the spectrum of the pulse Δn(ω) = -ρ e² / (2 n₀ ε₀ m_e ω (ω + iν)), ν = 1/τ_c (Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007)), +the first-order expansion in the density. The propagators apply it as a spectral operator, `dA/dz(ω) += i (ω/c) R(ω) FFT[Δn_ω₀(t) A(t)](ω)` +with R(ω) = ω₀ (ω₀ + iν) / (ω (ω + iν)) (R = 1 at the carrier, 0 at ω ≤ 0), so the blue-shifted leading edge of a broad pulse sees less plasma than the +carrier does, and the electrons born in the field take the drift energy they gain from it (U_p of the field at birth, in the loss budget as part of the +absorption). The operator is the same in the envelope, the carrier-resolved and the spatial models. It treats the density as slowly varying against the +optical cycle (the product ρ(t) A(t) is formed first, then the 1/(ω + iν) factor). `"frequencyResolved": false` gives the earlier carrier-frequency +value; the difference is first order in the bandwidth and for the filament example (40 fs, 800 nm) it is 2 nm in the centroid and 1 % in the energy. + +The drift energy U_p assumes that electrons are born with a uniform phase of the optical cycle, which is what the cycle-averaged rate of the envelope and spatial +models and the MPI rate say. In carrier-resolved mode PPT and ADK are born near the crests of the field, where the drift velocity is small, so the carrier-resolved model books +less than U_p per electron; the two models then differ by up to U_p per electron (about 6 eV against 15.8 eV for argon at 800 nm and 10¹⁴ W/cm², tested only with MPI, where they agree). +`absorption: false` removes the inverse-bremsstrahlung term of the Drude index only; the real part still carries the drift energy. With `selfSteepening: false` the other terms use the carrier +value of ω/c, the plasma term always uses ω/c at each frequency (as its current equation does). + +The ionisation loss Im Δn += U W ρ_n / (2 k₀ I) is a loss at the carrier, not a Drude term, and stays local in time. σ = k₀ ω₀ τ_c / (n₀ ρ_c (1 + ω₀² τ_c²)) is the avalanche cross +section (on by default for solids). The avalanche takes its energy from the inverse-bremsstrahlung absorption that the Drude imaginary part already books +(σρ I per volume, the same number), so it adds no second loss to the field; before 2.2.1 it did, and a solid with the avalanche on absorbed too much (`Plasma_Avalanche_DoesNotAbsorbTheFieldASecondTime`). With `absorption: false` neither is applied. +A run that gives only some of the solid inputs (`bandGapEv`, `effectiveMass`, `neutralDensityM3`, `collisionTimeS`, `recombinationTimeS`) gets the rest from the catalogue (or, for a material without ionisation data, from the +generic dielectric entry `defaults:solidIonization`); the run notes name the fields the run gave and the source of the others. Defaults: gases τ_c = 350 fs / p[bar], no recombination; solids τ_c = 1 fs, +τ_rec = 150 fs. `electronDensityM3` gives a plasma that is there before the pulse (a prepared plasma or a discharge); it decays with the recombination time. +Every element reports `peakElectronDensityM3` and `peakIonizationFraction`. In carrier-resolved mode (`physics.fieldMode: "carrier"`) PPT and ADK use the quasi-static rate at the instantaneous -field, so the sub-cycle steps of ionisation are resolved. In envelope mode the plasma term uses the carrier frequency -(the 1/ω² dispersion of the Drude index across a broad spectrum is approximated). +field, so the sub-cycle steps of ionisation are resolved; the Drude operator is the one above. Against it the envelope model agrees to 1 % in the energy +loss and 2 % in the blue shift for a 10 fs pulse at 2 µm (same MPI rate in both; test below). Limits: no negative frequencies (a pulse +broadened to zero frequency is cut, as stated in the capabilities), and a density that changes within a cycle (the carrier-resolved +ionisation steps) is only resolved in the rate, not in the Drude response. ## With spatial models: filaments @@ -54,4 +74,6 @@ focused; for paraxial beams it agrees with the envelope model (test `Uppe_Agrees ``` Tests: `IonizationTests` (Dawson and elliptic functions, PPT multiphoton order and tunnel limit, Keldysh order, -electron density = ρ_n ∫ W dt, blue shift and losses, filament clamping). +electron density = ρ_n ∫ W dt, blue shift and losses, filament clamping; a prepared plasma of fixed density against the Drude filter exp(i (ω/c) Δn(ω) L) +in the envelope, carrier-resolved and radial models to 1e-8, with 15 rad of phase; the narrow-band limit; the drift energy U_p of the electrons born in +the field to 2 %; envelope against carrier-resolved with ionisation for a 10 fs, 2 µm pulse). diff --git a/docs/reference/materials-catalogue.md b/docs/reference/materials-catalogue.md new file mode 100644 index 0000000..e1fcec6 --- /dev/null +++ b/docs/reference/materials-catalogue.md @@ -0,0 +1,776 @@ +# Material catalogue + +*Generated by `ultrafast reference materials` from `src/UltrafastEngine.Core/Data/materials/`; do not edit. Change a number in its catalogue file, then run `/regen`.* + +Every number the engine takes from its material library is listed here with its unit (in the key name), its **status**, its uncertainty and its source. +How the files work, how to override a value and how runs report the values they used: [Materials](materials.md#the-material-catalogue). + +| status | meaning | +|---|---| +| `verified` | the number was read from the cited source and compared; the comparison is recorded (`check`) | +| `approximate` | a typical, rounded or fitted value, good to the stated uncertainty; not traced to one table | +| `unverified` | not checked against any source; quoted from memory or from an earlier version of the code | + +A run that uses an `approximate` or `unverified` value says so in its notes and lists it under `unverifiedInputs` in `summary.json`. + +**185 quantities in 54 entries: 43 verified, 70 approximate, 70 unverified.** + +## Solids and liquids + +### Vacuum + +Vacuum. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = constant; value = 1 | verified | | definition | +| `n2` | valueM2PerW = 0 | verified | | definition | + +### FusedSilica + +Fused silica. Aliases: `F_SILICA`, `SILICA`, `FUSED_SILICA`, `FS`, `SIO2`, `UVFS`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [0.6961663, 0.4079426, 0.8974794]; resonanceWavelengthsUm = [0.0684043, 0.1162414, 9.896161]; validRangeUm = [0.21, 6.7] (temperatureK 293.15) | verified | | I. H. Malitson, J. Opt. Soc. Am. 55, 1205 (1965); refractiveindex.info main/SiO2/Malitson Check: agrees with main/SiO2/Malitson.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 2.6E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `raman` | | see model | | the Raman model 'blowWood' | +| `ionization` | bandGapEv = 9; effectiveMassRatio = 0.635; neutralDensityM3 = 2.1E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values for Keldysh / plasma models of fused silica; the original code did not record a source | + +### N-BK7 + +Schott N-BK7 borosilicate crown. Aliases: `BK7`, `NBK7`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [1.03961212, 0.231792344, 1.01046945]; poleUm2 = [0.00600069867, 0.0200179144, 103.560653]; validRangeUm = [0.3, 2.5] (temperatureK 293.15) | verified | | Schott N-BK7 data sheet (as listed on refractiveindex.info glass/schott/N-BK7) Check: agrees with glass/schott/N-BK7.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3.4E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 4.2; effectiveMassRatio = 0.5; neutralDensityM3 = 2E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values; the original code did not record a source | + +### CaF2 + +Calcium fluoride. Aliases: `CAF2`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [0.5675888, 0.4710914, 3.8484723]; resonanceWavelengthsUm = [0.050263605, 0.1003909, 34.64904]; validRangeUm = [0.23, 9.7] (temperatureK 293.15) | verified | | I. H. Malitson, Appl. Opt. 2, 1103 (1963) (via refractiveindex.info main/CaF2/Malitson) Check: agrees with main/CaF2/Malitson.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 1.9E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 10; effectiveMassRatio = 0.5; neutralDensityM3 = 2.4E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values; the original code did not record a source | + +### Sapphire + +Sapphire (Al2O3), ordinary ray. Aliases: `SAPPHIRE`, `AL2O3`, `SAPPHIRE-O`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [1.4313493, 0.65054713, 5.3414021]; resonanceWavelengthsUm = [0.0726631, 0.1193242, 18.028251] (temperatureK 293.15) | verified | | I. H. Malitson and M. J. Dodge, J. Opt. Soc. Am. 62, 1405 (1972) (via refractiveindex.info main/Al2O3/Malitson-o) Check: agrees with main/Al2O3/Malitson-o.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 8.8; effectiveMassRatio = 0.5; neutralDensityM3 = 2.3E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values; the original code did not record a source | + +### YAG + +Yttrium aluminium garnet. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [2.282, 3.27644]; poleUm2 = [0.01185, 282.734] (temperatureK 293.15) | verified | | D. E. Zelmon, D. L. Small, R. Page, Appl. Opt. 37, 4933 (1998) (via refractiveindex.info main/Y3Al5O12/Zelmon) Check: agrees with main/Y3Al5O12/Zelmon.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 6.2E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 6.5; effectiveMassRatio = 0.5; neutralDensityM3 = 2E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values; the original code did not record a source | + +### N-SF11 + +Schott N-SF11 dense flint. Aliases: `SF11`, `NSF11`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [1.73759695, 0.313747346, 1.89878101]; poleUm2 = [0.013188707, 0.0623068142, 155.23629] (temperatureK 293.15) | verified | | Schott N-SF11 data sheet (as listed on refractiveindex.info glass/schott/N-SF11) Check: agrees with glass/schott/N-SF11.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 1E-19 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | + +### MgF2 + +Magnesium fluoride, ordinary ray. Aliases: `MGF2`, `MGF2-O`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [0.48755108, 0.39875031, 2.3120353]; resonanceWavelengthsUm = [0.04338408, 0.09461442, 23.793604] (temperatureK 293.15) | verified | | M. J. Dodge, Appl. Opt. 23, 1980 (1984) (via refractiveindex.info main/MgF2/Dodge-o) Check: agrees with main/MgF2/Dodge-o.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 9E-21 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 10.8; effectiveMassRatio = 0.5; neutralDensityM3 = 2E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | typical values; the original code did not record a source | + +### Quartz + +Crystalline quartz, ordinary ray. Aliases: `QUARTZ`, `CRYSTALQUARTZ`, `SIO2_CRYSTAL`, `QUARTZ-O`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.28604141; b = [1.07044083, 1.10202242]; poleUm2 = [0.0100585997, 100] (temperatureK 293.15) | verified | | G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/SiO2/Ghosh-o) Check: agrees with main/SiO2/Ghosh-o.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 9; effectiveMassRatio = 0.635; neutralDensityM3 = 2.1E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | copy of the fused-silica values; the original code did not record a source | + +### Quartz-e + +Crystalline quartz, extraordinary ray. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.28851804; b = [1.09509924, 1.15662475]; poleUm2 = [0.0102101864, 100] (temperatureK 293.15) | verified | | G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/SiO2/Ghosh-e) Check: agrees with main/SiO2/Ghosh-e.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 9; effectiveMassRatio = 0.635; neutralDensityM3 = 2.1E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | copy of the fused-silica values; the original code did not record a source | + +### MgF2-e + +Magnesium fluoride, extraordinary ray. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [0.41344023, 0.50497499, 2.4904862]; resonanceWavelengthsUm = [0.03684262, 0.09076162, 23.771995] (temperatureK 293.15) | verified | | M. J. Dodge, Appl. Opt. 23, 1980 (1984) (via refractiveindex.info main/MgF2/Dodge-e) Check: agrees with main/MgF2/Dodge-e.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 9E-21 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 10.8; effectiveMassRatio = 0.5; neutralDensityM3 = 2E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | copy of the MgF2 values; the original code did not record a source | + +### Sapphire-e + +Sapphire (Al2O3), extraordinary ray. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1; b = [1.5039759, 0.55069141, 6.5927379]; resonanceWavelengthsUm = [0.0740288, 0.1216529, 20.072248] (temperatureK 293.15) | verified | | I. H. Malitson and M. J. Dodge, J. Opt. Soc. Am. 62, 1405 (1972) (via refractiveindex.info main/Al2O3/Malitson-e) Check: agrees with main/Al2O3/Malitson-e.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3E-20 (wavelengthM 8E-07) | approximate | about 30 % | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| `ionization` | bandGapEv = 8.8; effectiveMassRatio = 0.5; neutralDensityM3 = 2.3E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded) | copy of the sapphire values; the original code did not record a source | + +### Calcite + +Calcite, ordinary ray. Aliases: `CALCITE-O`, `CACO3`. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.73358749; b = [0.96464345, 1.82831454]; poleUm2 = [0.0194325203, 120] (temperatureK 293.15) | verified | | G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/CaCO3/Ghosh-o) Check: agrees with main/CaCO3/Ghosh-o.yml to 2.2e-16 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### Calcite-e + +Calcite, extraordinary ray. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.35859695; b = [0.8242783, 0.14429128]; poleUm2 = [0.0106689543, 120] (temperatureK 293.15) | verified | | G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/CaCO3/Ghosh-e) Check: agrees with main/CaCO3/Ghosh-e.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### YVO4 + +Yttrium orthovanadate, ordinary ray. Aliases: `YVO4-O`. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = vendorSellmeier; referenceMedium = air; a = 3.77834; numeratorUm2 = 0.069736; poleUm2 = 0.04724; irTermPerUm2 = 0.0108133 (temperatureK 293.15) | unverified | differs from H. S. Shi et al., J. Synthetic Cryst. 30, 85 (2001) at 20 C (refractiveindex.info main/YVO4/Shi-o-20C: A 3.778790, B 0.070479, C 0.045731, D 0.009701 in the same form) by -5.4e-4 at 633 nm, -5.9e-4 at 1064 nm; decision of the owner (2026-11-11): the vendor set stays | common vendor Sellmeier set; the original code did not record the vendor | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### YVO4-e + +Yttrium orthovanadate, extraordinary ray. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = vendorSellmeier; referenceMedium = air; a = 4.59905; numeratorUm2 = 0.110534; poleUm2 = 0.04813; irTermPerUm2 = 0.0122676 (temperatureK 293.15) | unverified | differs from H. S. Shi et al., J. Synthetic Cryst. 30, 85 (2001) at 20 C (refractiveindex.info main/YVO4/Shi-e-20C: A 4.607200, B 0.108087, C 0.052495, D 0.014305 in the same form) by -9.6e-4 at 633 nm, -9.2e-4 at 1064 nm; the birefringence differs by ~0.16 %; decision of the owner (2026-11-11): the vendor set stays | common vendor Sellmeier set; the original code did not record the vendor | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### aBBO + +alpha-BBO (alpha-barium borate), ordinary ray. Aliases: `ABBO`, `ALPHA-BBO`, `ABBO-O`, `A-BBO`. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = vendorSellmeier; referenceMedium = air; a = 2.67579; numeratorUm2 = 0.02099; poleUm2 = 0.0047; irTermPerUm2 = 0.00528 (temperatureK 293.15) | unverified | not stated in the original code | vendor Sellmeier set; the original code did not record the vendor | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### aBBO-e + +alpha-BBO (alpha-barium borate), extraordinary ray. + +n2 not set (give one if it matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = vendorSellmeier; referenceMedium = air; a = 2.31197; numeratorUm2 = 0.01184; poleUm2 = 0.01607; irTermPerUm2 = 0.004 (temperatureK 293.15) | unverified | not stated in the original code | vendor Sellmeier set; the original code did not record the vendor | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### TGG + +Terbium gallium garnet (Faraday rotator). + +dispersion fitted to the Schlarb & Sugg 1994 table (0.40-1.18 um, approximate); n2 not set (give one: the B-integral of an isolator matters). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.24206176; b = [2.49181577]; poleUm2 = [0.01959882]; validRangeUm = [0.40466, 1.17668] (temperatureK 293.15) | approximate | fit residual 6.6e-4 inside the measured range; extrapolation outside 0.40-1.18 um is not backed by data. The previous two-point fit (A = 1, B = 2.7769, C = 0.01656) was 0.011 away from the table at 1.08 um. | least-squares fit of n^2 = A + B l^2/(l^2 - C) to the nine measured points of U. Schlarb and B. Sugg, Phys. Status Solidi B 182, K91 (1994) (refractiveindex.info main/Tb3Ga5O12/Schlarb, 0.40466-1.17668 um, 20 C): largest deviation 6.6e-4 | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for this material (give one if it matters) | + +### BaF2 + +Barium fluoride, cubic (m3m). Aliases: `BAF2`. + +cubic (m3m); give the orientation with kerrCutAxis / kerrAzimuthDeg on the element (default [001] cut, x along [100]). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = air; a = 1.33973; b = [0.8107, 0.19652, 4.52469]; resonanceWavelengthsUm = [0.10065, 29.87, 53.82]; validRangeUm = [0.15, 15] (temperatureK 293.15) | verified | | H. H. Li, J. Phys. Chem. Ref. Data 9, 161 (1980), 293 K (refractiveindex.info main/BaF2/Li) Check: agrees with main/BaF2/Li.yml to 2.2e-16 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 2E-20 (wavelengthM 1E-06, orientation [100]) | unverified | about 30 % | typical literature value for light along [100]; reported values spread 1.8-2.9e-20 m^2/W; quoted from memory in the original code, not re-checked | +| `kerrAnisotropy` | value = -1.2 | unverified | unknown; check the papers. The Z-scan anisotropy paper of the Sheik-Bahae / Van Stryland group (CREOL publication 1820; a scanned PDF that could not be read here) gives BaF2 n2 along [100], [010], [110] at 532 and 1064 nm and a parameter u in chi(theta) = chi [1 + 2u (sin^4 theta - sin^2 theta)]; how u relates to sigma is still to be written down | sigma = (chi1111 - 3 chi1122) / chi1111; quoted from memory of Minkovski et al., Opt. Lett. 29, 2237 (2004) and Jullien et al., J. Opt. Soc. Am. B 22, 2635 (2005); not retrieved | + +### CS2 + +Liquid carbon disulfide, 20 C. Aliases: `CARBONDISULFIDE`, `CARBON_DISULFIDE`. + +liquid CS2 at 20 C; index fit valid 0.5-1.6 um; n2 = 3.0e-18 m^2/W total (ps pulses) of which 10 % electronic and 90 % orientational, h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps; the split and the time constants are unverified typical values (spread of the literature: a factor 2-3): override n2 / ramanFraction / raman. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = sellmeier; referenceMedium = vacuum; a = 1; b = [1.50387]; poleUm2 = [0.03049]; validRangeUm = [0.5, 1.6] (temperatureK 293.15) | verified | | S. Kedenburg et al., Opt. Mater. Express 2, 1588 (2012) (refractiveindex.info main/CS2/Kedenburg); one-pole fit valid 0.5-1.6 um, extrapolated outside Check: agrees with main/CS2/Kedenburg.yml to 0.0e+00 (n, 0.45-1.5 um). | +| `n2` | valueM2PerW = 3E-18 (wavelengthM 8E-07, pulseDurationS 1E-12, temperatureK 293.15) | unverified | about 100 % | total n2 for ps pulses (electronic + orientational); typical value, the literature spread is a factor 2-3; not re-checked | +| `raman` | | see model | | the Raman model 'orientational' | +| `ramanFraction` | value = 0.9 (pulseDurationS 1E-12) | unverified | about 10 % | orientational (delayed) share of the total n2; the electronic part 3e-19 m^2/W is the fs value quoted by Liu et al., Opt. Lett. 38, 1838 (2013) citing Ganeev 2004; not re-checked | + +### Ta2O5 + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `TA2O5`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [2.05, 0.02, 0.0012] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +### TiO2 + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `TIO2`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [2.2, 0.055, 0.003] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +### HfO2 + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `HFO2`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [1.95, 0.016, 0] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +### Nb2O5 + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `NB2O5`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [2.18, 0.04, 0] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +### Al2O3_film + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `AL2O3_FILM`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [1.62, 0.008, 0] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +### SiO2_film + +Ion-beam-sputtered thin film (Cauchy fit). Aliases: `SIO2_FILM`. + +thin-film Cauchy approximation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = cauchy; coefficients = [1.45, 0.0036, 0] | approximate | film indices depend on the process (a few 1e-2) | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| `n2` | valueM2PerW = 0 | unverified | 0 means not set, not a measured zero | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | + +## Gases + +### Air + +Dry air with 450 ppm CO2. Aliases: `AIR`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = ciddor; temperatureC = 20; pressurePa = 101325; relativeHumidityPercent = 0; co2Ppm = 450; borzsonyiCoefficients = [0.0001492644, 1.936E-05, 0.0004180757, 0.007434] (temperatureK 293.15, pressureBar 1.01325) | verified | | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (constants in the code); bundled check page other/air/Ciddor | +| `n2Electronic` | valueM2PerWPerBar273K = 8.5E-24 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked; air as a mixture of N2 and O2 | +| `composition` | fractions = {"N2":0.7808,"O2":0.2095,"Ar":0.0093} | verified | | mole fractions of dry air (standard atmosphere) | +| `standardAirDispersion` | k0PerUm2 = 238.0185; k1PerUm2 = 5792105; k2PerUm2 = 57.362; k3PerUm2 = 167917 (temperatureK 288.15, pressureBar 1.01325) | verified | | P. E. Ciddor, Appl. Opt. 35, 1566 (1996), dry-air dispersion: (n-1) 1e8 = k1 / (k0 - s^2) + k3 / (k2 - s^2), s = 1/lambda in 1/um; refractiveindex.info other/air/Ciddor (standard air 15 C, 101.325 kPa, 450 ppm CO2) Check: the page's coefficients 0.05792105, 238.0185, 0.00167917, 57.362 equal k1 / 1e8, k0, k3 / 1e8, k2; the engine at 15 C, 101325 Pa, dry, 450 ppm equals the page's formula (test CatalogueTests.CiddorAir_AtStandardConditions_EqualsTheDatabaseFormula). | +| `co2Dependence` | co2SlopeFractionPerPpm = 5.34E-07; co2ReferencePpm = 450; airMolarMassGPerMol = 28.9635; airMolarMassSlopeGPerMolPerPpm = 1.2011E-05; airMolarMassReferencePpm = 400 | unverified | not compared with the paper; the dry-air index at 633 nm is checked against the paper's value in FoundationTests | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (dispersion changes by 0.534e-6 per ppm of CO2 about 450 ppm; molar mass of dry air 28.9635 + 12.011e-6 (x_CO2 - 400) g/mol); not re-read | +| `waterVaporDispersion` | polynomialCoefficients = [295.235, 2.6422, -0.03238, 0.004028]; units = (n_ws - 1) 1e8 = cf (w0 + w1 s^2 + w2 s^4 + w3 s^6), s in 1/um; correctionFactor = 1.022 | unverified | humid-air results are not compared with the paper | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) water-vapour dispersion (w0..w3, cf); not re-read | +| `thermodynamics` | gasConstantJPerMolK = 8.31451; waterMolarMassKgPerMol = 0.018015; airMolarMassKgPerGPerMolFactor = 0.001 | unverified | constants of the paper; transcription not re-read | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (R = 8.314510 J/(mol K), M_w = 0.018015 kg/mol); not re-read | +| `saturationVapourPressure` | coefficients = [1.2378847E-05, -0.019121316, 33.93711047, -6343.1645]; units = p_sv = exp(A T^2 + B T + C + D / T) in Pa, T in K | unverified | not compared with the standard | CIPM-2007 / Ciddor 1996 saturation vapour pressure of water; transcription not re-read | +| `enhancementFactor` | coefficients = [1.00062, 3.14E-08, 5.6E-07]; units = f = a + b p + c t^2, p in Pa, t in degC | unverified | not compared with the standard | enhancement factor of water vapour in air (Ciddor 1996 / CIPM-2007); transcription not re-read | +| `compressibility` | coefficients = [1.58123E-06, -2.9331E-08, 1.1043E-10, ... (9 values)]; units = a0, a1, a2, b0, b1, c0, c1, d, e of Z(p, T, x_w) | unverified | not compared with the standard | compressibility factor of moist air (Ciddor 1996 / CIPM-2007); transcription not re-read | +| `referenceStates` | standardAirPressurePa = 101325; standardAirTemperatureK = 288.15; waterVaporPressurePa = 1333; waterVaporTemperatureK = 293.15 | approximate | definitions; transcription not re-read | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) reference states of the density ratios | + +### He + +Helium. Aliases: `HELIUM`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [4.97777E-05, 2.854E-05, 1.85694E-05, 0.00776]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/He/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/He/Borzsonyi: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 3.5E-25 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 24.587; orbitalL = 0; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | + +### Ne + +Neon. Aliases: `NEON`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [9.15448E-05, 0.00065697, 4.01863E-05, 0.005728]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Ne/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/Ne/Borzsonyi: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 7.5E-25 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 21.565; orbitalL = 1; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | + +### Ar + +Argon. Aliases: `ARGON`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [0.0002033229, 0.00020612, 0.0003445831, 0.008066]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Ar/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/Ar/Borzsonyi: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 1E-23 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 15.76; orbitalL = 1; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | + +### Kr + +Krypton. Aliases: `KRYPTON`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [0.0002610288, 2.01E-06, 0.0005694682, 0.010043]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Kr/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/Kr/Borzsonyi: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 2.8E-23 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 13.999; orbitalL = 1; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | + +### Xe + +Xenon. Aliases: `XENON`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [0.0010370161, 0.01275, 0.0003122861, 0.000561]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Xe/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/Xe/Borzsonyi: max \|dn\| = 0.0e+00 (the page fixes a typo of the paper in Xe: 12.75e-3). | +| `n2Electronic` | valueM2PerWPerBar273K = 8E-23 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 12.13; orbitalL = 1; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | + +### N2 + +Nitrogen. Aliases: `NITROGEN`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = borzsonyi; coefficients = [0.0003920995, 0.00114624, 0.0001880648, 0.013476]; validRangeUm = [0.4, 1] (temperatureK 273, pressureBar 1.01325) | verified | | A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/N2/Borzsonyi), 273 K and 1 bar Check: agrees vs refractiveindex.info main/N2/Borzsonyi: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 8E-24 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 15.581; orbitalL = 0; zEff = 0.9 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice | +| `rotation` | rotationalConstantBPerCm = 1.98957; centrifugalDistortionDPerCm = 5.76E-06; polarizabilityAnisotropyA3 = 0.7; nuclearSpinWeightEven = 6; nuclearSpinWeightOdd = 3 (temperatureK 293.15) | approximate | about 15 % | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| `dephasing` | t2At1BarS = 1E-10 (pressureBar 1, temperatureK 293.15) | unverified | about 50 % | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | + +### O2 + +Oxygen. Aliases: `OXYGEN`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = dispersionFormula; c0 = 0.0001181494; b = [0.009708931]; cPerUm2 = [75.4]; referenceTemperatureK = 293.15 (temperatureK 293.15, pressureBar 1.01325) | verified | | Zhang et al., Appl. Opt. 47, 3143 (2008), 293.15 K (refractiveindex.info main/O2/Zhang) Check: agrees with main/O2/Zhang.yml: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 1.03E-23 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 12.07; orbitalL = 1; zEff = 0.53 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice | +| `rotation` | rotationalConstantBPerCm = 1.43768; centrifugalDistortionDPerCm = 4.85E-06; polarizabilityAnisotropyA3 = 1.14; nuclearSpinWeightEven = 0; nuclearSpinWeightOdd = 1 (temperatureK 293.15) | approximate | about 15 % | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| `dephasing` | t2At1BarS = 8E-11 (pressureBar 1, temperatureK 293.15) | unverified | about 50 % | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | + +### CO2 + +Carbon dioxide. Aliases: `CARBONDIOXIDE`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = dispersionFormula; c0 = 0; b = [0.06991, 0.0014472, 6.42941E-05, 5.21306E-05, 1.46847E-06]; cPerUm2 = [166.175, 79.609, 56.3064, 46.0196, 0.0584738]; referenceTemperatureK = 273.15 (temperatureK 273.15, pressureBar 1.01325) | verified | | Bideau-Mehu et al., Opt. Commun. 9, 432 (1973), 273.15 K (refractiveindex.info main/CO2/Bideau-Mehu) Check: agrees with main/CO2/Bideau-Mehu.yml: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 1.4E-23 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 13.777; orbitalL = 1; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| `rotation` | rotationalConstantBPerCm = 0.39021; centrifugalDistortionDPerCm = 1.33E-07; polarizabilityAnisotropyA3 = 2.1; nuclearSpinWeightEven = 1; nuclearSpinWeightOdd = 0 (temperatureK 293.15) | approximate | about 15 % | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| `dephasing` | t2At1BarS = 5E-11 (pressureBar 1, temperatureK 293.15) | unverified | about 50 % | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | + +### H2 + +Hydrogen. Aliases: `HYDROGEN`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `index` | model = dispersionFormula; c0 = 0; b = [0.0148956, 0.0049037]; cPerUm2 = [180.7, 92]; referenceTemperatureK = 273.15 (temperatureK 273.15, pressureBar 1.01325) | verified | | Peck and Huang, J. Opt. Soc. Am. 67, 1550 (1977), 273.15 K (refractiveindex.info main/H2/Peck) Check: agrees with main/H2/Peck.yml: max \|dn\| = 0.0e+00. | +| `n2Electronic` | valueM2PerWPerBar273K = 6.9E-24 (wavelengthM 8E-07, pressureBar 1, temperatureK 273) | approximate | about 20 % | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| `ionization` | ionizationEv = 15.426; orbitalL = 0; zEff = 1 | approximate | ionisation potentials are tabulated constants; the effective charge and orbital l are model choices | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| `rotation` | rotationalConstantBPerCm = 59.322; centrifugalDistortionDPerCm = 0.0471; polarizabilityAnisotropyA3 = 0.3; nuclearSpinWeightEven = 1; nuclearSpinWeightOdd = 3 (temperatureK 293.15) | approximate | about 15 % | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| `dephasing` | t2At1BarS = 3E-10 (pressureBar 1, temperatureK 293.15) | unverified | about 50 % | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | + +## Raman models + +### blowWood + +Fused silica, single damped oscillator. Aliases: `silica`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `response` | model = dampedOscillator; tau1S = 1.22E-14; tau2S = 3.2E-14 (host fused silica) | approximate | about 10 % | K. J. Blow and D. Wood, IEEE J. Quantum Electron. 25, 2665 (1989); the constants 12.2 fs and 32 fs are the ones in common use for fused silica | +| `ramanFraction` | value = 0.18 (host fused silica) | approximate | about 20 % | Blow-Wood / Agrawal, Nonlinear Fiber Optics: fR = 0.18 for fused silica; Lin and Agrawal (2006) give 0.245 with the boson peak | +| `anisotropicFraction` | value = 0.25 (host fused silica) | approximate | about 30 % | anisotropic share of the Raman response (Lin and Agrawal fb + fc = 0.25), from the measured depolarisation of the Raman spectrum | + +### hollenbeck + +Fused silica, 13 vibrational modes. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `response` | model = hollenbeckCantrell; modePositionsPerCm = [56.25, 100, 231.25, ... (13 values)]; peakIntensities = [1, 11.4, 36.67, ... (13 values)]; gaussianFwhmPerCm = [52.1, 110.42, 175, ... (13 values)]; lorentzianFwhmPerCm = [17.37, 38.81, 58.33, ... (13 values)] (host fused silica) | unverified | transcribed table of 13 modes, not re-read against the paper; the response is checked for the 13.2 THz gain peak | D. Hollenbeck and C. D. Cantrell, J. Opt. Soc. Am. B 19, 2886 (2002), Table 1 (intermediate-broadening model), as transcribed in the original code; not re-read | +| `ramanFraction` | value = 0.18 (host fused silica) | approximate | about 20 % | same as the Blow-Wood model | +| `anisotropicFraction` | value = 0.25 (host fused silica) | approximate | about 30 % | same as the Blow-Wood model | + +### linAgrawal + +Fused silica with boson peak. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `response` | model = linAgrawal; tau1S = 1.22E-14; tau2S = 3.2E-14; taubS = 9.6E-14; fa = 0.75; fb = 0.21; fc = 0.04 (host fused silica) | approximate | about 10 % | Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006); pyNLO's 'full' model uses the same numbers | +| `ramanFraction` | value = 0.245 (host fused silica) | approximate | about 10 % | Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006) | + +### orientational + +Molecular reorientation of a liquid (CS2 values). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `response` | model = orientational; riseS = 2E-13; decayS = 1.6E-12; anisotropicFraction = 1.5 (host CS2, temperatureK 293.15) | unverified | about 30 % | h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps: typical CS2 values of the original code (decay ~1.6-2 ps is the commonly quoted reorientation time); the rise time and the exact decay are unverified; 1.5 = purely orientational (h_a = -h_b/3) | + +## Gain media + +### Yb:YAG + +Ytterbium-doped YAG (quasi-three-level). Aliases: `ybyag`. + +Yb:YAG cross sections are an analytic room-temperature fit; the McCumber relation gives reabsorption at the lasing wavelength (~0.1e-20 cm^2 at 1030 nm). Cryogenic linewidths are not modelled (only the Boltzmann factor follows temperatureK). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `doping` | host = YAG; dopingDensityM3 = 2.76E+26 | approximate | user-specific: set dopingDensityM3 or dopingAtPercent | doping density of the original code (2 x 1.38e26 m^-3); give your crystal's doping | +| `lifetime` | lifetimeS = 0.00095 | approximate | about 10 % | Sumida and Fan, OSA TOPS 1995 (upper-state lifetime 0.95 ms), as cited by the original code; not re-read | +| `zeroLine` | zeroLineWavelengthM = 9.688E-07 | approximate | about 0.1 % | zero-phonon line 968.8 nm of Yb:YAG, as used by the original code for the McCumber relation; not re-read | +| `thermal` | thermalConductivityWPerMK = 9.5; dnDTPerK = 7.8E-06 | unverified | not stated in the original code | typical values of Yb:YAG (original code); conductivity falls with doping | +| `stress` | thermalExpansionPerK = 7.5E-06; stressCr = 0.017; stressCphi = -0.0025 | unverified | not stated in the original code | photo-elastic constants of YAG after Koechner, Solid-State Laser Engineering (thermal-effects chapter), as cited by the original code; not re-read | +| `emission` | shape = gaussians; bands = [{"wavelengthM":1.03E-06,"sigmaCm2":2.1E-20,"fwhmM":8.5E-09}, {"wavelengthM":1.0485E-06,"sigmaCm2":3.2E-21,"fwhmM":1.6E-08}, {"wavelengthM":1.015E-06,"sigmaCm2":3.5E-21,"fwhmM":3E-08}]; description = Gaussian-band fit: 2.1e-20 cm^2 at 1030 nm (8.5 nm FWHM), 1048 nm shoulder, broad base (temperatureK 293.15) | approximate | about 20 % | room-temperature values after Koerner et al., Appl. Phys. B 75, 2002 (cross sections) and Sumida & Fan, OSA TOPS 1995 (lifetime 0.95 ms); lasing-band absorption and pump-band emission from the McCumber relation (zero line 968.8 nm); approximate analytic fit (~10-20 %) -- override with measured tables | +| `absorption` | shape = gaussians; bands = [{"wavelengthM":9.405E-07,"sigmaCm2":7.7E-21,"fwhmM":1.8E-08}, {"wavelengthM":9.688E-07,"sigmaCm2":8.5E-21,"fwhmM":2.8E-09}, {"wavelengthM":9.15E-07,"sigmaCm2":2.5E-21,"fwhmM":1.5E-08}]; description = Gaussian-band fit: 0.77e-20 cm^2 at 940 nm (18 nm FWHM), 0.85e-20 cm^2 at 969 nm (2.8 nm) (temperatureK 293.15) | approximate | about 20 % | pump-band absorption of Yb:YAG, approximate analytic fit (~10-20 %), after the same references as the emission band -- override with measured tables | + +### Ti:sapphire + +Titanium-doped sapphire (four-level). Aliases: `tisa`, `tisapphire`, `ti:sa`, `ti:al2o3`. + +Ti:sapphire is treated as a four-level system (no lasing-band absorption); the residual infrared absorption (figure of merit) is not included -- add it with absorptionProfile or a material attenuation. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `doping` | host = Sapphire; dopingDensityM3 = 3.3E+25 | approximate | user-specific: set dopingDensityM3 | doping density of the original code; give your crystal's doping | +| `lifetime` | lifetimeS = 3.2E-06 | approximate | about 10 % | Moulton, JOSA B 3, 125 (1986) (lifetime 3.2 us), as cited by the original code; not re-read | +| `thermal` | thermalConductivityWPerMK = 33; dnDTPerK = 1.3E-05 | unverified | not stated in the original code | typical values of sapphire (original code) | +| `emission` | shape = asymmetricGaussian; peakWavelengthM = 7.95E-07; sigmaCm2 = 3E-19; hwhmShortM = 7.5E-08; hwhmLongM = 1.4E-07; description = asymmetric-Gaussian fit, pi polarisation: 3.0e-19 cm^2 at 795 nm, half widths 75 / 140 nm | approximate | about 20 % | after Moulton, JOSA B 3, 125 (1986) (pi-polarised cross sections, lifetime 3.2 us); four-level; approximate analytic fit (~10-20 %) -- override with measured tables | +| `absorption` | shape = gaussians; bands = [{"wavelengthM":4.9E-07,"sigmaCm2":6.4E-20,"fwhmM":1.15E-07}]; description = Gaussian fit, pi polarisation: 6.4e-20 cm^2 at 490 nm, 115 nm FWHM (~4.4e-20 cm^2 at 532 nm) | approximate | about 20 % | after Moulton, JOSA B 3, 125 (1986) (pi-polarised); approximate analytic fit (~10-20 %) -- override with measured tables | + +### Nd:YAG + +Neodymium-doped YAG (four-level). Aliases: `ndyag`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `doping` | host = YAG; dopingDensityM3 = 1.38E+26 | approximate | user-specific: set dopingDensityM3 | 1 at.% Nd (Koechner), as in the original code | +| `lifetime` | lifetimeS = 0.00023 | approximate | about 10 % | Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us), as cited by the original code; not re-read | +| `thermal` | thermalConductivityWPerMK = 10.5; dnDTPerK = 7.8E-06 | unverified | not stated in the original code | typical values of Nd:YAG (original code) | +| `stress` | thermalExpansionPerK = 7.5E-06; stressCr = 0.017; stressCphi = -0.0025 | unverified | not stated in the original code | photo-elastic constants of YAG after Koechner (as for Yb:YAG); not re-read | +| `emission` | shape = gaussians; bands = [{"wavelengthM":1.0642E-06,"sigmaCm2":2.8E-19,"fwhmM":6E-10}]; description = Gaussian fit: 2.8e-19 cm^2 at 1064.2 nm, 0.6 nm FWHM | approximate | about 20 % | Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us); four-level; approximate analytic fit | +| `absorption` | shape = gaussians; bands = [{"wavelengthM":8.085E-07,"sigmaCm2":7E-20,"fwhmM":2.5E-09}]; description = Gaussian fit: 7e-20 cm^2 at 808.5 nm, 2.5 nm FWHM | approximate | about 20 % | Koechner, Solid-State Laser Engineering; approximate analytic fit | + +## chi(2) crystals + +### BBO + +beta-barium borate, negative uniaxial. Aliases: `b-bab2o4`, `bab2o4`, `beta-bbo`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula2; coefficients = [0, 0.90291, 0.003926, ... (7 values)]; validRangeUm = [0.188, 5.2]; citation = Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (temperatureK 293.15) | verified | | Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (refractiveindex.info BaB2O4/Tamosauskas-o) Check: agrees with the coefficients of the fetched page BaB2O4_Tamosauskas-o (formula 2, 7 coefficients). | +| `indexZ` | model = riiFormula2; coefficients = [0, 1.151075, 0.007142, ... (7 values)]; validRangeUm = [0.188, 5.2]; citation = Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (temperatureK 293.15) | verified | | Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (refractiveindex.info BaB2O4/Tamosauskas-e) Check: agrees with the coefficients of the fetched page BaB2O4_Tamosauskas-e (formula 2, 7 coefficients). | +| `d` | pointGroup = 3m; tensor = 3m; dPmPerV = {"d22":2.2,"d31":0.04} (wavelengthM 1.064E-06) | unverified | about 10 % | Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read | +| `n2` | valueM2PerW = 2.8E-20 (wavelengthM 8E-07) | unverified | about 30 % | DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read | +| `validity` | validRangeUm = [0.188, 5.2] | approximate | range of the index data; outside it the formula is extrapolated | Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) | + +### LBO + +lithium triborate, biaxial; the Kato (1994) set reproduces the handbook XY-plane type-I SHG angles (phi = 11.6 deg at 1064 nm, 13.8 deg at 1030 nm, 31.7 deg at 800 nm), the Chen / Hanson-Dick fit on refractiveindex.info gives 11.8 / 14.6 / 34.3 deg. Aliases: `lib3o5`. + +LBO indices are room-temperature (no thermal model): non-critical phase matching by temperature is not modelled. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula4; coefficients = [2.4542, 0.01125, 0, ... (11 values)]; validRangeUm = [0.19, 2.6]; citation = Kato, IEEE J. Quantum Electron. 30, 2950 (1994) (temperatureK 293.15) | unverified | not compared with the database page (none for this form) | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| `indexY` | model = riiFormula4; coefficients = [2.539, 0.01277, 0, ... (15 values)]; validRangeUm = [0.19, 2.6]; citation = Kato, IEEE J. Quantum Electron. 30, 2950 (1994) (temperatureK 293.15) | unverified | not compared with the database page (none for this form) | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| `indexZ` | model = riiFormula4; coefficients = [2.5865, 0.0131, 0, ... (15 values)]; validRangeUm = [0.19, 2.6]; citation = Kato, IEEE J. Quantum Electron. 30, 2950 (1994) (temperatureK 293.15) | unverified | not compared with the database page (none for this form) | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| `d` | pointGroup = mm2 (polar axis = Y); tensor = mm2:y; dPmPerV = {"d31":-0.67,"d32":0.85,"d33":0.04} (wavelengthM 1.064E-06) | unverified | about 10 % | Dmitriev, Gurzadyan & Nikogosyan, Handbook of Nonlinear Optical Crystals, as cited by the original code; not re-read | +| `n2` | valueM2PerW = 1.9E-20 (wavelengthM 8E-07) | unverified | about 30 % | approximate value of the original code (no source recorded) | +| `validity` | validRangeUm = [0.19, 2.6] | approximate | range of the index data; outside it the formula is extrapolated | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | + +### KTP + +potassium titanyl phosphate, biaxial (the aliases ppktp name the periodically poled form). Aliases: `ktiopo4`, `ppktp`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula4; coefficients = [3.291, 0.0414, 0, ... (9 values)]; validRangeUm = [0.43, 3.54]; citation = Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (temperatureK 293.15) | verified | | Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-alpha) Check: agrees with the coefficients of the fetched page KTiOPO4_Kato-alpha (formula 4, 9 coefficients). | +| `indexY` | model = riiFormula4; coefficients = [3.45018, 0.04341, 0, ... (9 values)]; validRangeUm = [0.43, 3.54]; citation = Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (temperatureK 293.15) | verified | | Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-beta) Check: agrees with the coefficients of the fetched page KTiOPO4_Kato-beta (formula 4, 9 coefficients). | +| `indexZ` | model = riiFormula4; coefficients = [4.59423, 0.06206, 0, ... (9 values)]; validRangeUm = [0.43, 3.54]; citation = Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (temperatureK 293.15) | verified | | Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-gamma) Check: agrees with the coefficients of the fetched page KTiOPO4_Kato-gamma (formula 4, 9 coefficients). | +| `d` | pointGroup = mm2; tensor = mm2; dPmPerV = {"d31":1.95,"d32":3.9,"d33":15.3} (wavelengthM 1.064E-06) | unverified | about 10 % | Vanherzeele & Bierlein, Opt. Lett. 17, 982 (1992), as cited by the original code; not re-read | +| `n2` | valueM2PerW = 2.2E-19 (wavelengthM 8E-07) | unverified | about 30 % | DeSalvo et al., IEEE JQE 32, 1324 (1996), E\|\|z, as cited by the original code; not re-read | +| `validity` | validRangeUm = [0.43, 3.54] | approximate | range of the index data; outside it the formula is extrapolated | Kato & Takaoka, Appl. Opt. 41, 5040 (2002) | + +### KDP + +potassium dihydrogen phosphate, negative uniaxial. Aliases: `kh2po4`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula4; coefficients = [2.259276, 13.00522, 2, ... (9 values)]; validRangeUm = [0.2138, 1.529]; citation = Zernike, JOSA 54, 1215 (1964) (temperatureK 293.15) | verified | | Zernike, JOSA 54, 1215 (1964) (refractiveindex.info KH2PO4/Zernike-o) Check: agrees with the coefficients of the fetched page KH2PO4_Zernike-o (formula 4, 9 coefficients). | +| `indexZ` | model = riiFormula4; coefficients = [2.132668, 3.2279924, 2, ... (9 values)]; validRangeUm = [0.2138, 1.529]; citation = Zernike, JOSA 54, 1215 (1964) (temperatureK 293.15) | verified | | Zernike, JOSA 54, 1215 (1964) (refractiveindex.info KH2PO4/Zernike-e) Check: agrees with the coefficients of the fetched page KH2PO4_Zernike-e (formula 4, 9 coefficients). | +| `d` | pointGroup = -42m; tensor = -42m; dPmPerV = {"d36":0.39} (wavelengthM 1.064E-06) | unverified | about 10 % | Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read | +| `n2` | valueM2PerW = 2E-20 (wavelengthM 8E-07) | unverified | about 30 % | Adair et al. (via refractiveindex.info), as cited by the original code; not re-read | +| `validity` | validRangeUm = [0.2138, 1.529] | approximate | range of the index data; outside it the formula is extrapolated | Zernike, JOSA 54, 1215 (1964) | + +### LiNbO3 + +congruent lithium niobate, negative uniaxial; the aliases ppln, cln name the poled / congruent forms. Aliases: `ln`, `cln`, `ppln`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula2; coefficients = [0, 2.6734, 0.01764, ... (7 values)]; validRangeUm = [0.4, 5]; citation = Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (temperatureK 293.15) | verified | | Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (refractiveindex.info LiNbO3/Zelmon-o) Check: agrees with the coefficients of the fetched page LiNbO3_Zelmon-o (formula 2, 7 coefficients). | +| `indexZ` | model = jundt; a = [5.35583, 0.100473, 0.20692, 100, 11.34927, 0.015334]; b = [4.629E-07, 3.862E-08, -8.9E-09, 2.657E-05]; citation = Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray; validRangeUm = [0.4, 5] (temperatureK 297.65) | unverified | not compared with the paper | Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray (temperature dependent, T in degC) | +| `d` | pointGroup = 3m; tensor = 3m; dPmPerV = {"d22":2.1,"d31":4.6,"d33":25} (wavelengthM 1.064E-06) | unverified | about 10 % | Shoji et al., JOSA B 14, 2268 (1997), as cited by the original code; not re-read | +| `n2` | valueM2PerW = 9.3E-20 (wavelengthM 8E-07) | unverified | about 30 % | DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read | +| `validity` | validRangeUm = [0.4, 5] | approximate | range of the index data; outside it the formula is extrapolated | Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) | + +### MgO:LiNbO3 + +5 mol% MgO-doped congruent lithium niobate. Aliases: `mgln`, `mgo:ln`, `mgo:ppln`, `ppmgln`, `mgo:cln`. + +MgO:LiNbO3: the o-ray index is that of congruent LiNbO3 (only e-ray data for the doped crystal are built in); type-0 (eee) QPM is unaffected. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula2; coefficients = [0, 2.6734, 0.01764, ... (7 values)]; validRangeUm = [0.4, 5]; citation = Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (temperatureK 293.15) | verified | | Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (refractiveindex.info LiNbO3/Zelmon-o) Check: agrees with the coefficients of the fetched page LiNbO3_Zelmon-o (formula 2, 7 coefficients). | +| `indexZ` | model = jundt; a = [5.756, 0.0983, 0.202, 189.32, 12.52, 0.0132]; b = [2.86E-06, 4.7E-08, 6.113E-08, 0.0001516]; citation = Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray; validRangeUm = [0.5, 4] (temperatureK 297.65) | unverified | the temperature coefficient b4 = 1.516e-4 could not be cross-checked (pyNLO uses 1.526e-5) | Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray (temperature dependent, T in degC) | +| `d` | pointGroup = 3m; tensor = 3m; dPmPerV = {"d22":2.1,"d31":4.4,"d33":25} (wavelengthM 1.064E-06) | unverified | about 10 % | d33 = 25 pm/V as cited by the original code; not re-read | +| `n2` | valueM2PerW = 9.3E-20 (wavelengthM 8E-07) | unverified | about 30 % | copy of the congruent LiNbO3 value (DeSalvo et al. 1996); not re-read | +| `validity` | validRangeUm = [0.5, 4] | approximate | range of the index data; outside it the formula is extrapolated | Gayer et al., Appl. Phys. B 91, 343 (2008) | + +### BiBO + +bismuth triborate, biaxial (monoclinic: no d tensor built in). Aliases: `bib3o6`. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula4; coefficients = [3.07403, 0.03231, 0, ... (11 values)]; validRangeUm = [0.48, 3.1]; citation = Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (temperatureK 293.15) | verified | | Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-alpha) Check: agrees with the coefficients of the fetched page BiB3O6_Umemura-alpha (formula 4, 11 coefficients). | +| `indexY` | model = riiFormula4; coefficients = [3.1694, 0.03717, 0, ... (11 values)]; validRangeUm = [0.48, 3.1]; citation = Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (temperatureK 293.15) | verified | | Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-beta) Check: agrees with the coefficients of the fetched page BiB3O6_Umemura-beta (formula 4, 11 coefficients). | +| `indexZ` | model = riiFormula4; coefficients = [3.6545, 0.05112, 0, ... (11 values)]; validRangeUm = [0.48, 3.1]; citation = Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (temperatureK 293.15) | verified | | Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-gamma) Check: agrees with the coefficients of the fetched page BiB3O6_Umemura-gamma (formula 4, 11 coefficients). | +| `n2` | valueM2PerW = 7.65E-20 (wavelengthM 8E-07) | unverified | about 30 % | Kabacinski et al. (via refractiveindex.info), as cited by the original code; not re-read | +| `validity` | validRangeUm = [0.48, 3.1] | approximate | range of the index data; outside it the formula is extrapolated | Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) | + +### AgGaSe2 + +silver gallium selenide, negative uniaxial (mid-infrared). + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `indexX` | model = riiFormula4; coefficients = [11.62264, 0.43221, 0, ... (9 values)]; validRangeUm = [0.81, 18]; citation = Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (temperatureK 293.15) | verified | | Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (refractiveindex.info AgGaSe2/Kato-o) Check: agrees with the coefficients of the fetched page AgGaSe2_Kato-o (formula 4, 9 coefficients). | +| `indexZ` | model = riiFormula4; coefficients = [11.33168, 0.45951, 0, ... (9 values)]; validRangeUm = [0.81, 18]; citation = Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (temperatureK 293.15) | verified | | Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (refractiveindex.info AgGaSe2/Kato-e) Check: agrees with the coefficients of the fetched page AgGaSe2_Kato-e (formula 4, 9 coefficients). | +| `d` | pointGroup = -42m; tensor = -42m; dPmPerV = {"d36":33} (wavelengthM 1.064E-06) | unverified | about 10 % | d36 = 33 pm/V of the original code; no source recorded | +| `n2` | valueM2PerW = 3.5E-18 (wavelengthM 8E-07) | unverified | about 100 % | approximate value of the original code (no source recorded) | +| `validity` | validRangeUm = [0.81, 18] | approximate | range of the index data; outside it the formula is extrapolated | Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) | + +## Generic defaults (used when a run does not give the value) + +### defaults:gainMedium + +What the engine assumes for a user-defined gain medium that does not state its own thermal data. Used for the thermal lens only.. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `thermal` | thermalConductivityWPerMK = 10; dnDTPerK = 7.8E-06 | unverified | a factor of 2 for other hosts (sapphire 33 W/(m K), glasses ~1 W/(m K); dn/dT changes sign between hosts) | generic YAG-like values chosen in the original code; not tied to a measurement | + +### defaults:solidIonization + +What the engine assumes for a solid without ionisation data when a run gives only some of the inputs (the band gap is always required).. + +| quantity | values | status | uncertainty | source | +|---|---|---|---|---| +| `ionization` | effectiveMassRatio = 0.635; neutralDensityM3 = 2.1E+28; collisionTimeS = 1E-15; recombinationTimeS = 1.5E-13 | unverified | sample-dependent by tens of percent, and different for other dielectrics | typical Keldysh / plasma inputs of fused silica, used as the generic dielectric (original code; no source recorded) | + +## Not verified yet + +The to-do list for anyone with the papers: every quantity whose status is not `verified`. + +| entry | quantity | status | what to check | +|---|---|---|---| +| Air | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked; air as a mixture of N2 and O2 | +| Air | `co2Dependence` | unverified | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (dispersion changes by 0.534e-6 per ppm of CO2 about 450 ppm; molar mass of dry air 28.9635 + 12.011e-6 (x_CO2 - 400) g/mol); not re-read | +| Air | `waterVaporDispersion` | unverified | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) water-vapour dispersion (w0..w3, cf); not re-read | +| Air | `thermodynamics` | unverified | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (R = 8.314510 J/(mol K), M_w = 0.018015 kg/mol); not re-read | +| Air | `saturationVapourPressure` | unverified | CIPM-2007 / Ciddor 1996 saturation vapour pressure of water; transcription not re-read | +| Air | `enhancementFactor` | unverified | enhancement factor of water vapour in air (Ciddor 1996 / CIPM-2007); transcription not re-read | +| Air | `compressibility` | unverified | compressibility factor of moist air (Ciddor 1996 / CIPM-2007); transcription not re-read | +| Air | `referenceStates` | approximate | P. E. Ciddor, Appl. Opt. 35, 1566 (1996) reference states of the density ratios | +| BBO | `d` | unverified | Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read | +| BBO | `n2` | unverified | DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read | +| BBO | `validity` | approximate | Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) | +| Yb:YAG | `doping` | approximate | doping density of the original code (2 x 1.38e26 m^-3); give your crystal's doping | +| Yb:YAG | `lifetime` | approximate | Sumida and Fan, OSA TOPS 1995 (upper-state lifetime 0.95 ms), as cited by the original code; not re-read | +| Yb:YAG | `zeroLine` | approximate | zero-phonon line 968.8 nm of Yb:YAG, as used by the original code for the McCumber relation; not re-read | +| Yb:YAG | `thermal` | unverified | typical values of Yb:YAG (original code); conductivity falls with doping | +| Yb:YAG | `stress` | unverified | photo-elastic constants of YAG after Koechner, Solid-State Laser Engineering (thermal-effects chapter), as cited by the original code; not re-read | +| Yb:YAG | `emission` | approximate | room-temperature values after Koerner et al., Appl. Phys. B 75, 2002 (cross sections) and Sumida & Fan, OSA TOPS 1995 (lifetime 0.95 ms); lasing-band absorption and pump-band emission from the McCumber relation (zero line 968.8 nm); approximate analytic fit (~10-20 %) -- override with measured tables | +| Yb:YAG | `absorption` | approximate | pump-band absorption of Yb:YAG, approximate analytic fit (~10-20 %), after the same references as the emission band -- override with measured tables | +| defaults:gainMedium | `thermal` | unverified | generic YAG-like values chosen in the original code; not tied to a measurement | +| FusedSilica | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| FusedSilica | `ionization` | unverified | typical values for Keldysh / plasma models of fused silica; the original code did not record a source | +| LBO | `indexX` | unverified | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| LBO | `indexY` | unverified | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| LBO | `indexZ` | unverified | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| LBO | `d` | unverified | Dmitriev, Gurzadyan & Nikogosyan, Handbook of Nonlinear Optical Crystals, as cited by the original code; not re-read | +| LBO | `n2` | unverified | approximate value of the original code (no source recorded) | +| LBO | `validity` | approximate | Kato, IEEE J. Quantum Electron. 30, 2950 (1994) | +| Ti:sapphire | `doping` | approximate | doping density of the original code; give your crystal's doping | +| Ti:sapphire | `lifetime` | approximate | Moulton, JOSA B 3, 125 (1986) (lifetime 3.2 us), as cited by the original code; not re-read | +| Ti:sapphire | `thermal` | unverified | typical values of sapphire (original code) | +| Ti:sapphire | `emission` | approximate | after Moulton, JOSA B 3, 125 (1986) (pi-polarised cross sections, lifetime 3.2 us); four-level; approximate analytic fit (~10-20 %) -- override with measured tables | +| Ti:sapphire | `absorption` | approximate | after Moulton, JOSA B 3, 125 (1986) (pi-polarised); approximate analytic fit (~10-20 %) -- override with measured tables | +| defaults:solidIonization | `ionization` | unverified | typical Keldysh / plasma inputs of fused silica, used as the generic dielectric (original code; no source recorded) | +| KTP | `d` | unverified | Vanherzeele & Bierlein, Opt. Lett. 17, 982 (1992), as cited by the original code; not re-read | +| KTP | `n2` | unverified | DeSalvo et al., IEEE JQE 32, 1324 (1996), E\|\|z, as cited by the original code; not re-read | +| KTP | `validity` | approximate | Kato & Takaoka, Appl. Opt. 41, 5040 (2002) | +| N-BK7 | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| N-BK7 | `ionization` | unverified | typical values; the original code did not record a source | +| Nd:YAG | `doping` | approximate | 1 at.% Nd (Koechner), as in the original code | +| Nd:YAG | `lifetime` | approximate | Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us), as cited by the original code; not re-read | +| Nd:YAG | `thermal` | unverified | typical values of Nd:YAG (original code) | +| Nd:YAG | `stress` | unverified | photo-elastic constants of YAG after Koechner (as for Yb:YAG); not re-read | +| Nd:YAG | `emission` | approximate | Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us); four-level; approximate analytic fit | +| Nd:YAG | `absorption` | approximate | Koechner, Solid-State Laser Engineering; approximate analytic fit | +| CaF2 | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| CaF2 | `ionization` | unverified | typical values; the original code did not record a source | +| KDP | `d` | unverified | Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read | +| KDP | `n2` | unverified | Adair et al. (via refractiveindex.info), as cited by the original code; not re-read | +| KDP | `validity` | approximate | Zernike, JOSA 54, 1215 (1964) | +| LiNbO3 | `indexZ` | unverified | Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray (temperature dependent, T in degC) | +| LiNbO3 | `d` | unverified | Shoji et al., JOSA B 14, 2268 (1997), as cited by the original code; not re-read | +| LiNbO3 | `n2` | unverified | DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read | +| LiNbO3 | `validity` | approximate | Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) | +| Sapphire | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| Sapphire | `ionization` | unverified | typical values; the original code did not record a source | +| MgO:LiNbO3 | `indexZ` | unverified | Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray (temperature dependent, T in degC) | +| MgO:LiNbO3 | `d` | unverified | d33 = 25 pm/V as cited by the original code; not re-read | +| MgO:LiNbO3 | `n2` | unverified | copy of the congruent LiNbO3 value (DeSalvo et al. 1996); not re-read | +| MgO:LiNbO3 | `validity` | approximate | Gayer et al., Appl. Phys. B 91, 343 (2008) | +| YAG | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| YAG | `ionization` | unverified | typical values; the original code did not record a source | +| BiBO | `n2` | unverified | Kabacinski et al. (via refractiveindex.info), as cited by the original code; not re-read | +| BiBO | `validity` | approximate | Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) | +| N-SF11 | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| AgGaSe2 | `d` | unverified | d36 = 33 pm/V of the original code; no source recorded | +| AgGaSe2 | `n2` | unverified | approximate value of the original code (no source recorded) | +| AgGaSe2 | `validity` | approximate | Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) | +| MgF2 | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| MgF2 | `ionization` | unverified | typical values; the original code did not record a source | +| He | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| He | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| Ne | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| Ne | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| Ar | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| Ar | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| Kr | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| Kr | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| Xe | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| Xe | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| N2 | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| N2 | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice | +| N2 | `rotation` | approximate | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| N2 | `dephasing` | unverified | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | +| O2 | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| O2 | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice | +| O2 | `rotation` | approximate | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| O2 | `dephasing` | unverified | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | +| CO2 | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| CO2 | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| CO2 | `rotation` | approximate | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| CO2 | `dephasing` | unverified | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | +| H2 | `n2Electronic` | approximate | electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked | +| H2 | `ionization` | approximate | ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices | +| H2 | `rotation` | approximate | rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry | +| H2 | `dephasing` | unverified | rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it | +| Quartz | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| Quartz | `ionization` | unverified | copy of the fused-silica values; the original code did not record a source | +| Quartz-e | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| Quartz-e | `ionization` | unverified | copy of the fused-silica values; the original code did not record a source | +| MgF2-e | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| MgF2-e | `ionization` | unverified | copy of the MgF2 values; the original code did not record a source | +| Sapphire-e | `n2` | approximate | typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked | +| Sapphire-e | `ionization` | unverified | copy of the sapphire values; the original code did not record a source | +| Calcite | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| Calcite-e | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| YVO4 | `index` | unverified | common vendor Sellmeier set; the original code did not record the vendor | +| YVO4 | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| YVO4-e | `index` | unverified | common vendor Sellmeier set; the original code did not record the vendor | +| YVO4-e | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| aBBO | `index` | unverified | vendor Sellmeier set; the original code did not record the vendor | +| aBBO | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| aBBO-e | `index` | unverified | vendor Sellmeier set; the original code did not record the vendor | +| aBBO-e | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| TGG | `index` | approximate | least-squares fit of n^2 = A + B l^2/(l^2 - C) to the nine measured points of U. Schlarb and B. Sugg, Phys. Status Solidi B 182, K91 (1994) (refractiveindex.info main/Tb3Ga5O12/Schlarb, 0.40466-1.17668 um, 20 C): largest deviation 6.6e-4 | +| TGG | `n2` | unverified | not set: no value was entered for this material (give one if it matters) | +| BaF2 | `n2` | unverified | typical literature value for light along [100]; reported values spread 1.8-2.9e-20 m^2/W; quoted from memory in the original code, not re-checked | +| BaF2 | `kerrAnisotropy` | unverified | sigma = (chi1111 - 3 chi1122) / chi1111; quoted from memory of Minkovski et al., Opt. Lett. 29, 2237 (2004) and Jullien et al., J. Opt. Soc. Am. B 22, 2635 (2005); not retrieved | +| CS2 | `n2` | unverified | total n2 for ps pulses (electronic + orientational); typical value, the literature spread is a factor 2-3; not re-checked | +| CS2 | `ramanFraction` | unverified | orientational (delayed) share of the total n2; the electronic part 3e-19 m^2/W is the fs value quoted by Liu et al., Opt. Lett. 38, 1838 (2013) citing Ganeev 2004; not re-checked | +| Ta2O5 | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| Ta2O5 | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| TiO2 | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| TiO2 | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| HfO2 | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| HfO2 | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| Nb2O5 | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| Nb2O5 | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| Al2O3_film | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| Al2O3_film | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| SiO2_film | `index` | approximate | generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs | +| SiO2_film | `n2` | unverified | not set: no value was entered for thin films (give one if the B-integral of a coating matters) | +| blowWood | `response` | approximate | K. J. Blow and D. Wood, IEEE J. Quantum Electron. 25, 2665 (1989); the constants 12.2 fs and 32 fs are the ones in common use for fused silica | +| blowWood | `ramanFraction` | approximate | Blow-Wood / Agrawal, Nonlinear Fiber Optics: fR = 0.18 for fused silica; Lin and Agrawal (2006) give 0.245 with the boson peak | +| blowWood | `anisotropicFraction` | approximate | anisotropic share of the Raman response (Lin and Agrawal fb + fc = 0.25), from the measured depolarisation of the Raman spectrum | +| hollenbeck | `response` | unverified | D. Hollenbeck and C. D. Cantrell, J. Opt. Soc. Am. B 19, 2886 (2002), Table 1 (intermediate-broadening model), as transcribed in the original code; not re-read | +| hollenbeck | `ramanFraction` | approximate | same as the Blow-Wood model | +| hollenbeck | `anisotropicFraction` | approximate | same as the Blow-Wood model | +| linAgrawal | `response` | approximate | Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006); pyNLO's 'full' model uses the same numbers | +| linAgrawal | `ramanFraction` | approximate | Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006) | +| orientational | `response` | unverified | h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps: typical CS2 values of the original code (decay ~1.6-2 ps is the commonly quoted reorientation time); the rise time and the exact decay are unverified; 1.5 = purely orientational (h_a = -h_b/3) | diff --git a/docs/reference/materials.md b/docs/reference/materials.md index 7331959..84af0c7 100644 --- a/docs/reference/materials.md +++ b/docs/reference/materials.md @@ -3,6 +3,8 @@ ## Built-in library Names are case-insensitive; OpticStudio names are accepted as aliases (`F_SILICA`, `BK7`, `CAF2`, `SAPPHIRE`, ...). +Every number in this table lives in a catalogue file with its source and status: see [the material catalogue](#the-material-catalogue) and the +generated page [Material catalogue](materials-catalogue.md), which is the complete and current list (this table is a summary). | name | index model | reference | n2 (m^2/W) | delayed response | |---|---|---|---|---| @@ -22,7 +24,7 @@ Names are case-insensitive; OpticStudio names are accepted as aliases (`F_SILICA | `TGG` | approximate two-point fit | air | none | Faraday rotators; give your crystal's data | | `Ta2O5`, `TiO2`, `HfO2`, `Nb2O5`, `Al2O3_film`, `SiO2_film` | Cauchy fits (thin films) | | | | -The nonlinear coefficients are typical values near 0.8-1 um; published values spread by ~10-30 % +The nonlinear coefficients are typical values near 0.8-1 um (status `approximate` in the catalogue); published values spread by ~10-30 % (wavelength, measurement method, pulse duration). Override them for quantitative work: `{"target": "material:FusedSilica", "property": "n2", "value": 2.2e-20}`. Thin-film indices are generic ion-beam-sputtered values: use your coating vendor's data for real designs. @@ -50,6 +52,64 @@ Use `"raman": {"kind": "orientational", "riseS": ..., "decayS": ..., "fR": ...}` (for light along [100]) and the Kerr anisotropy `kerrAnisotropy` sigma = -1.2 are typical literature values, **UNVERIFIED** (sigma quoted from memory of Minkovski et al., Opt. Lett. 29, 2237 (2004) and Jullien et al., J. Opt. Soc. Am. B 22, 2635 (2005)). See [cubic anisotropy](nonlinear.md#cubic-kerr-anisotropy-xpw). +## The material catalogue + +Every number the library hands to the engine (dispersion coefficients, n2, Raman time constants and fractions, gas constants, ionisation data) is stored in a **catalogue +file**, not in C#. One JSON file per material, gas or Raman model, in `src/UltrafastEngine.Core/Data/materials/{solids,gases,raman}/` (embedded in the library, so the CLI, the +Python package and the C API all see the same numbers). Each *quantity* of an entry (`index`, `n2`, `rotation`, `ionization`, ...) carries: + +| key | meaning | +|---|---| +| the numbers | with the unit in the key name (`valueM2PerW`, `tau1S`, `ionizationEv`, `rotationalConstantBPerCm`); coefficients keep the units of their source (resonance wavelengths in um) | +| `source` | the paper, table or database page the number comes from | +| `status` | `verified` (read from the source and compared), `approximate` (typical, rounded or fitted), `unverified` (not checked) | +| `relativeUncertainty` / `uncertainty` | the literature spread (for example a factor 2-3 for the n2 of CS2) | +| `conditions` | wavelength, temperature, pressure, pulse duration the value applies to | +| `verification` | for a verified dispersion set: the comparison that was made | + +The complete list, with a "not verified yet" table at the end, is [Material catalogue](materials-catalogue.md) (generated by `ultrafast reference materials`). + +**Runs say what they used.** When a nonlinear element uses an `approximate` or `unverified` catalogue value, the run notes say so +(`APPROXIMATE catalogue value used: FusedSilica.n2 ...`) and `summary.json` lists it under `unverifiedInputs` (quantity, material, status, source, uncertainty). +Only values the run really used are listed: n2 and the Raman response only for a nonlinear element, the dispersion for every element that uses the material's index, the +ionisation data only when `ionization` is on, and nothing the system file or the run spec overrides. A linear run with an unverified index lists the index. + +**Your own catalogue.** A JSON file in `~/.ultrafast/materials` (or in the folder named by the environment variable `ULTRAFAST_MATERIALS`; an empty value switches the +user catalogue off) adds a material or replaces single quantities of a built-in one. The file needs the `id` and only the quantities that change; each quantity needs a `source` +and a `status` like the built-in ones. A run that uses a replaced value says `user catalogue value used` in its notes, and `Catalogue.Notices` names every replacement. + +```jsonc +// ~/.ultrafast/materials/FusedSilica.json: use my own Z-scan value of n2, keep everything else +{ "id": "FusedSilica", + "quantities": { "n2": { "valueM2PerW": 2.19e-20, "source": "own Z-scan, 800 nm, 2026-03", "status": "verified" } } } +``` + +For a single run, an [override](models.md) in the run spec is simpler: it replaces the value for that run only and removes the notice. + +**Gain media, chi(2) crystals and the air index are in the catalogue too.** `gain/` holds the built-in gain media (`Yb:YAG`, `Ti:sapphire`, `Nd:YAG`: lifetime, doping density, zero-phonon line, thermal +and stress constants, emission and absorption bands in cm², each quantity with its source and status); `crystals/` holds the chi(2) crystals (`BBO`, `LBO`, `KTP`, `KDP`, `LiNbO3`, `MgO:LiNbO3`, +`BiBO`, `AgGaSe2`: the three principal-index formulas, the d tensor in pm/V, n2, the validity range). The constants of Ciddor's air-index formulas (dispersion, CO2 and water-vapour terms, saturation vapour pressure, +compressibility) are quantities of the `Air` entry. A gain element or crystal element that uses an `approximate` / `unverified` value of its medium reports it like a material does +(`Yb:YAG.emission`, `BBO.d`); thermal data are reported only with a thermal lens, stress constants only with stress birefringence, and a `deffPmPerV`, `dopingDensityM3` or a user-defined +band replaces the catalogue's quantity. In the system file these values keep working as before (`"medium": {"base": "Yb:YAG", "lifetimeS": ...}`, `"crystal": {"base": "BBO", ...}`). + +### Strict mode + +`"physics": {"strictMaterials": true}` in the run spec (or the environment variable `ULTRAFAST_STRICT_MATERIALS=1`, for CI and for results that are going into a paper) makes the compile **fail** when the run uses any +catalogue value that is not `verified`. The error lists each one (material, quantity, status, source). Ways out: override the value for the run (an [override](models.md) or a property of the +material), give a verified value in [your own catalogue file](#the-material-catalogue), or switch the mode off. Values that are not used are not counted (n2 and Raman in a linear run, thermal data +without a thermal lens), and a value from your own file counts as verified only if you marked it `verified`. + +To accept one named value and still refuse the others, list it by its catalogue path (as the error and `unverifiedInputs` print it; an entry name accepts all its quantities): + +```jsonc +{ "physics": { "strictMaterials": true } } // run.json: refuse approximate / unverified catalogue values +{ "physics": { "strictMaterials": true, "strictMaterialsAllow": ["FusedSilica.n2", "CS2"] } } // accept those, refuse the rest +``` + +The generic values used where a run gives none (the thermal data of a user-defined gain medium, the ionisation inputs of a solid without data) are catalogue entries too (`defaults:gainMedium`, `defaults:solidIonization`, +status `unverified`): a run that uses one lists it like any other value, and strict mode refuses it unless it is overridden or allowed. + ## Defining materials Any material can be defined in the system file (or overridden from the run spec) with the model kinds in diff --git a/docs/reference/modulator.md b/docs/reference/modulator.md index 41540e4..872f548 100644 --- a/docs/reference/modulator.md +++ b/docs/reference/modulator.md @@ -51,7 +51,7 @@ fringing fields do and why the efficiency of a real SLM falls near the jumps. * **An array** in the system file (`"phaseRad": [[...], ...]`; the Python builder converts numpy arrays). Fine for small maps. * **A callback** `"phaseRad": "@name"`: `f(x, y, z) → values` evaluated at the cell centres when the run compiles (z = 0, device frame), registered like a position-dependent field: `uf.run(system, spec, fields={"name": f})` in Python, `registry.RegisterCallback("name", (FieldCallback)...)` in C#. - No file and no JSON, so a 1920 × 1080 map costs one array. A trial of a 128 × 128 loop costs 10 ms with a callback or array against 46 ms with a new file each time ([spikes](../design/notes/spikes.md)). + No file and no JSON, so a 1920 × 1080 map costs one array. A trial of a 128 × 128 loop costs 10 ms with a callback or array against 46 ms with a new file each time ([spikes](../design/history.md)). **Loops:** every run compiles once and reads the callback then, so a control loop that changes what the callback returns gets the new map at each trial (tested: N trials equal N runs from N map files to 1e-12). The C API registers the same callback with `uf_run_set_field_callback`. * **A file**: `"file": "map.npz"` with `phase_rad` (or `opd_m`) and `amplitude`. @@ -78,7 +78,7 @@ pixel-integration effect of the real device. Consequences, measured in the tests Reference behaviour (tests): a smooth ramp `k θ x` under `opd` deflects every colour by the same angle θ; the same ramp *wrapped* at 2π (a blazed grating of period `p`) leaves at `λ/p`, so the first order moves in proportion to λ (measured 760/800/840 nm in the -[spike](../design/notes/spikes.md), both as a `mask file` and as a thin `bulk` with an index profile); under `fixedPhase` the same ramp +[spike](../design/history.md), both as a `mask file` and as a thin `bulk` with an index profile); under `fixedPhase` the same ramp deflects by `θ λ/λ_design`. Under `index` a wrapped blaze has the depth `2π α` with `α = ω Δn(ω) / (ω_design Δn(ω_design))` at each frequency, so its first-order efficiency off the design wavelength follows the staircase of that depth (tested at 1.2 µm, α = 0.833 against 0.858 for `opd`). Device data (a measured Δn(λ), voltage curves) stays in plugins, which hand the core the map and the Δn table. @@ -89,7 +89,7 @@ The profile is stored at `transverse.spectralSamples` frequencies and interpolat enough samples. The element reports **`spectralSamplingError`** (the largest distance of a stored sample from the line through its two neighbours, in units of the field norm; it falls about 1/K² with the number K of samples) and warns above 0.05. It cannot see a phase step near a multiple of 2π per stored frequency, so the element also warns when the phase changes by more than π/2 between stored frequencies. Compare K and 2K for the quantity you care about. Measured (30 fs, 800 nm, a smooth random screen, error of the focal-plane map against K = 96): 8 to 16 -samples give 1e-3 or better up to 30 rad of phase, K ≥ 32 for 100 rad ([spikes](../design/notes/spikes.md)). +samples give 1e-3 or better up to 30 rad of phase, K ≥ 32 for 100 rad ([spikes](../design/history.md)). **Size and time** (CPU, Apple silicon): a 1920 × 1080 panel with a 30 fs pulse takes 32 s and 3.6 GB at K = 16, 3.3 s and 1.05 GB for one frequency (cw). A 4096 × 4096 grid takes 20 s and 3.4 GB for one frequency. For long chains on large grids see diff --git a/docs/reference/options.md b/docs/reference/options.md index 3702f82..b70faef 100644 --- a/docs/reference/options.md +++ b/docs/reference/options.md @@ -94,7 +94,7 @@ The time/frequency grid (docs/reference/run-spec.md#grid). | `windowS` | number | s | 30 x the transform limit | Time window | | `windowPs` | number | ps | | Time window in ps | | `maxN` | integer | | 4194304 | Largest allowed n | -| `snapCarrier` | boolean | | true | Put the carrier on the frequency grid | +| `snapCarrier` | boolean | | true | Put the carrier on the frequency grid. A cw source is not snapped (its line sits on centerWavelengthM) unless this is written | | `spectralMargin` | number | | auto | Spectral span factor | | `minSpanFraction` | number | | 0.1 | Nonlinear runs: minimum half-span / carrier | | `minPointsPerFwhm` | number | | 12 | Samples across the transform-limited FWHM | @@ -117,6 +117,8 @@ Physics switches (docs/reference/run-spec.md#physics). | `fieldMode` | `"auto"` \| `"envelope"` \| `"carrier"` | | auto | Envelope or carrier-resolved (real field) propagation | | `fieldHarmonic` | number | | 3 | Highest harmonic the carrier-resolved grid holds | | `absoluteIndex` | boolean | | true | Add air dispersion to air paths quoted relative to air | +| `strictMaterialsAllow` | array | | | Catalogue quantities that strictMaterials accepts although they are not verified, by catalogue path (e.g. "CS2.n2") as listed in the error and in summary.json unverifiedInputs; matching is case-insensitive and an entry name such as "CS2" accepts all its quantities | +| `strictMaterials` | boolean | | false | Refuse a run that uses approximate or unverified material-catalogue values (the compile error lists them; override each, or give a verified value in your own catalogue file). The environment variable ULTRAFAST_STRICT_MATERIALS=1 does the same | | `polarization` | `"jones"` \| `"scalar"` | | jones | Jones (two polarisations) or scalar | | `losses` | boolean | | true | false removes flat loss terms | | `beamTracking` | any | | "auto" | Gaussian beam (q) tracking: "auto" (on with a Gaussian inputBeam), "on", "off" | @@ -1293,7 +1295,7 @@ Keys of gain and amplifier elements. | `beam` | model (beam) | | | Signal beam | | `modeRadiusM` | number | m | | Signal 1/e^2 radius (mode area pi w^2 / 2) | | `transverse` | object (`transverse`) | | | Spatial gain | -| `thermalLens` | any | | | true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens) | +| `thermalLens` | any | | | true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens) | | `slices` | integer | | 24 | Slices along z | | `segments` | integer | | auto | Time-ordered saturation groups | | `spm` | boolean | | true | SPM of the host | @@ -1303,6 +1305,7 @@ Keys of gain and amplifier elements. | `dopingProfile` | model (field) | | | Position-dependent doping factor | | `ase` | boolean | | false | Seed amplified spontaneous emission (1-D) | | `aseSeed` | integer | | | ASE random seed | +| `aseModel` | any | | false | true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase) | | `temperatureK` | number | K | | Crystal temperature | | `dopingDensityM3` | number | 1/m^3 | | Doping density (overrides the medium's) | @@ -1894,6 +1897,8 @@ Photo-ionisation and plasma (docs/reference/ionization.md). | `collisionTimeS` | number | s | | Electron collision time | | `recombinationTimeS` | number | s | | Recombination / trapping time | | `recombinationBetaM3PerS` | number | m^3/s | | Two-body recombination | +| `electronDensityM3` | number | 1/m^3 | 0 | Free-electron density present before the pulse (a prepared plasma) | +| `frequencyResolved` | boolean | | true | Apply the Drude index at every frequency of the pulse (1/(omega (omega + i nu))); false: at the carrier only | | `avalanche` | boolean | | true for solids | Avalanche (impact) ionisation | | `absorption` | boolean | | true | Plasma and ionisation losses | | `species` | string | | | Gas species (when the material does not say) | diff --git a/docs/reference/speed.md b/docs/reference/speed.md index 9355677..498cd8a 100644 --- a/docs/reference/speed.md +++ b/docs/reference/speed.md @@ -111,3 +111,12 @@ Parameter studies freeze the grid, cache the unchanged prefix of the system and * `"stepper": {"tolerance": 1e-5}` is usually enough; the default 1e-6 is conservative. * Install FFTW on Windows and Linux (on macOS Accelerate is already there): it is the cheapest speed-up. * `"compute": {"backend": "auto"}` is safe to leave on: without a GPU, or for small elements, it runs on the CPU. + +## Measured limits, and when to revisit them + +Two speed-ups were considered and not built (owner decision, 2026-11-10). Each has the measurement that decided it and the condition that reopens it. + +* **A cache for the compiled system.** In a loop of many trials (sensorless optimisation, a fit) a trial on a 128 x 128 grid costs 10 ms *including* the compile, so a cache would save very little. **Revisit if** a loop over a much larger system shows the compile taking more than 30 % of the time of a trial. +* **A fully streaming executor** (never holding all transverse frequency slices in memory). The peak memory of a large pulse run is set by the .NET garbage collector, not by the propagator: 2.7 to 3.4 GB at 1920 x 1080 with 16 stored frequencies, whatever the block size. **Revisit if** a 4096 x 4096 pulse run is needed, and measure it first: only the 1920 x 1080 case has been measured. A cw run at 4096 x 4096 needs 3.4 GB and 20 s today. + +Both are recorded with their numbers in [the S2 spikes](../design/history.md). diff --git a/docs/reference/transverse.md b/docs/reference/transverse.md index c3492de..fb6b690 100644 --- a/docs/reference/transverse.md +++ b/docs/reference/transverse.md @@ -209,7 +209,7 @@ Two settings keep long chains on large grids (a full 1920 × 1080 SLM panel, man `used` (one profile is 268 MB), bit-identical instruments. Peak memory *during* a run is set by the .NET garbage collector, which returns large freed arrays late: on the 1920 × 1080 × 16 case of the -[spikes](../design/notes/spikes.md) the process peak stayed at 2.7-3.4 GB whatever the block size, so `frequencyBlock` lowers the live set +[spikes](../design/history.md) the process peak stayed at 2.7-3.4 GB whatever the block size, so `frequencyBlock` lowers the live set but not the reported peak. A 1920 × 1080 single-frequency panel on a 2048 × 1280 grid (modulator, 0.2 m gap, beam profile) takes 1.9 s and holds 260 MB after the run with `keepProfiles: "used"`, 491 MB with `all` (slow test `FullPanel_1920x1080_SingleFrequency_RunsWithinTheSpikeBudget`). diff --git a/docs/reference/validation-matrix.md b/docs/reference/validation-matrix.md index 29a5b08..502c937 100644 --- a/docs/reference/validation-matrix.md +++ b/docs/reference/validation-matrix.md @@ -2,15 +2,18 @@ -Every C# test of the engine: **553 tests** in 59 classes. **349** of them check the engine against something outside it -- an analytic result, a published value or another code -- named in the *reference* column with the agreement required. **19** are cross-checks between two parts of the engine (a reference starting with "cross-"): they catch inconsistency, not a shared error. The others test behaviour and bookkeeping. The narrative overview is [Testing and validation](../testing.md); a test declares its reference with `[Test(Reference = "...", Tolerance = "...")]`. +Every C# test of the engine: **598 tests** in 62 classes. **381** of them check the engine against something outside it -- an analytic result, a published value or another code -- named in the *reference* column with the agreement required. **20** are cross-checks between two parts of the engine (a reference starting with "cross-"): they catch inconsistency, not a shared error. The others test behaviour and bookkeeping. The narrative overview is [Testing and validation](../testing.md); a test declares its reference with `[Test(Reference = "...", Tolerance = "...")]`. | area | class | tests | with an external reference | |---|---|---|---| -| [AO plugin: deformable mirror, SLM, spectral shaper and AOPDF elements](#aodevicetests) | `AoDeviceTests` | 20 | 20 | -| [Beam sources and masks: Hermite-Gauss, Laguerre-Gauss, Bessel, Airy, measured near fields, axicon, zone plate, vortex, slits](#beamsourcetests) | `BeamSourceTests` | 13 | 12 | -| [Gaussian beam tracking: thermal lenses and spatio-temporal couplings](#beamtrackingtests) | `BeamTrackingTests` | 13 | 12 | +| [AO plugin: deformable mirror, SLM, spectral shaper and AOPDF elements](#aodevicetests) | `AoDeviceTests` | 26 | 26 | +| [Inter-pulse ASE (both directions, spectrally resolved) and the stress-birefringence pattern](#asestresstests) | `AseStressTests` | 9 | 9 | +| [Beam sources and masks: Hermite-Gauss, Laguerre-Gauss, Bessel, Airy, measured near fields, axicon, zone plate, vortex, slits](#beamsourcetests) | `BeamSourceTests` | 15 | 14 | +| [Gaussian beam tracking: thermal lenses and spatio-temporal couplings](#beamtrackingtests) | `BeamTrackingTests` | 17 | 16 | | [Branched systems: splitter family, Gouy and crystal phases, mode overlap, profiles meeting, crystal ports](#branchedelementstests) | `BranchedElementsTests` | 12 | 8 | | [Branched systems: ports, graph compiler, interference, loops](#branchingtests) | `BranchingTests` | 18 | 8 | +| [Material catalogue: values pinned, provenance, user catalogues](#cataloguesnapshottests) | `CatalogueSnapshotTests` | 1 | 1 | +| [Material catalogue: values pinned, provenance, user catalogues](#cataloguetests) | `CatalogueTests` | 16 | 4 | | [Resonators, partial coherence, thermal stress birefringence, amplifier thermal lens](#cavityandcoherencetests) | `CavityAndCoherenceTests` | 9 | 8 | | [Multi-pass cell modes (Kerr-matched Herriott and quasi-concentric cells)](#cellmodetests) | `CellModeTests` | 7 | 7 | | [χ(2) crystals: SHG, OPA, QPM, cascading, phase matching, aux beams](#chi2tests) | `Chi2Tests` | 14 | 7 | @@ -34,7 +37,7 @@ Every C# test of the engine: **553 tests** in 59 classes. **349** of them check | [Instrument device models: resolution, acceptance, detectors, filters](#instrumentdevicetests) | `InstrumentDeviceTests` | 8 | 3 | | [Instrument fixes: band edges, IAC peak, cross-correlator resolution and arms](#instrumentfixestests) | `InstrumentFixesTests` | 5 | 5 | | [Instruments: autocorrelators, spectrometer, compressor, polarimeter, d-scan](#instrumenttests) | `InstrumentTests` | 4 | 3 | -| [Photo-ionisation and plasma](#ionizationtests) | `IonizationTests` | 5 | 5 | +| [Photo-ionisation and plasma](#ionizationtests) | `IonizationTests` | 11 | 11 | | [Lasers: Haus master equation, absorbers, cavities, apertures, carrier-resolved χ(2)](#lasertests) | `LaserTests` | 12 | 7 | | [Component sheet: linear elements and splitters at 515 nm / 1030 nm / 2 um, 1 ps / 10 fs, Sequential = Branched](#linearcomponenttests) | `LinearComponentTests` | 5 | 4 | | [Loss budget and overrides](#lossandoverridetests) | `LossAndOverrideTests` | 4 | 1 | @@ -61,7 +64,7 @@ Every C# test of the engine: **553 tests** in 59 classes. **349** of them check | [Large spatial runs: frequency streaming, profile retention, cylindrical lens, full-panel budget](#spatialstreamingtests) | `SpatialStreamingTests` | 6 | 6 | | [Spatial (transverse) models](#spatialtests) | `SpatialTests` | 6 | 5 | | [FFTs, SIMD kernels and studies](#speedstudytests) | `SpeedStudyTests` | 6 | 2 | -| [Coherent tile source: N sub-beams with piston, tilt and delay; combined far field, grating lobes, phase and delay errors](#tilesourcetests) | `TileSourceTests` | 10 | 10 | +| [Coherent tile source: N sub-beams with piston, tilt and delay; combined far field, grating lobes, phase and delay errors](#tilesourcetests) | `TileSourceTests` | 11 | 11 | | [Fitting plugin: transforms, backward propagation, fits](#transformtests) | `TransformTests` | 20 | 3 | | [The test framework and the generated validation matrix](#validationmatrixtests) | `ValidationMatrixTests` | 2 | 0 | | [Zemax plugin: export and replay](#zemaxtests) | `ZemaxTests` | 5 | 1 | @@ -80,18 +83,40 @@ AO plugin: deformable mirror, SLM, spectral shaper and AOPDF elements. | `DriveBits_QuantiseTheCommandsToTwoToTheBMinusOneSteps` | drive resolution: a b-bit drive over +-stroke has 2^b - 1 steps, level k = -stroke + k step with step = 2 stroke / (2^b - 1); a tilt ramp driven with driveBits 3 equals the same ramp rounded by hand to those levels and driven continuously | 1e-12 (focus centroid) | | `Erf_MatchesTabulatedValues` | erf by series and continued fraction against tabulated values (Abramowitz and Stegun table 7.1): erf(0.5) = 0.5204998778130465, erf(1) = 0.8427007929497149, erf(2) = 0.9953222650189527, erf(3.5) = 0.9999992569016276 | 1e-12 | | `HexLayout_HasSixNeighboursAtOnePitch` | hex layout: odd rows shifted by half a pitch, row spacing pitch sqrt(3)/2, so every interior actuator has six neighbours at one pitch | 1e-12 | +| `Planner_Aopdf_FixedWindow_WarnsAboutWrap` | fixed window (grid.auto false): a programmed GDD x bandwidth stretch of about 1 ns on a 10 ps window wraps, so the planner warns | a warning that says the grid is too small | +| `Planner_Aopdf_Gdd_WidensTheWindow` | GDD x bandwidth: a transform-limited Gaussian of FWHM tau and GDD phi'' has FWHM tau sqrt(1 + (4 ln2 phi''/tau^2)^2) (Diels and Rudolph, Ultrashort Laser Pulse Phenomena, ch. 1); the auto-sized window must hold it | window >= 2 x the stretched FWHM, stretched FWHM within 2 % of the formula | +| `Planner_SpectralShaper_FlatMask_AddsNoRoom` | a flat mask makes no side pulses: the planner adds no room (same grid as an aopdf without programmed phase) | identical grid | +| `Planner_SpectralShaper_WidensTheWindow_ForItsSidePulses` | Weiner (2000): side pulses of a pixelated mask lie at 2 pi / pixel bandwidth; the planner must hold them without grid.windowS (window >= 2 x 1.25 x T, T = 1.6 ps here) | window at least 2 x 1.25 x T; no warning; the +T side pulse is in the output | | `Slm_BinaryPhaseGrating_OrdersSitAtWavelengthTimesFocalLengthOverPeriod` | binary phase grating (4 pixels at phase 0, 4 at pi) through two grey levels: diffraction orders at +-lambda f / (8 pixels) carrying the sampled-staircase Fourier weight, none at the zero order (a pi step has c_0 = 0) | 0.6 % of the order efficiency; zero order below 1e-4 | | `Slm_FillFactor_SetsTheTransmission_AndTheReplicaAtWavelengthTimesFocalLengthOverPitch` | pixel replicas: a panel of pixels with fill factor 3/4 (4 grid cells per pixel, edges on cell edges) transmits 3/4 of the amplitude-squared... power 3/4 and has the discrete Fourier series c_0 = 3/4, \|c_1\| = 1/4, so the replica at lambda f / pitch carries 1/9 of the zero order (same geometry as the core modulator comb test) | 1e-6 transmission, 1 % order ratio | -| `Slm_FullPanel_1920x1080_8Bit_RunsWithinTheScaleBudget` (slow) | S2 scale spike (docs/design/notes/spikes.md): a 1920 x 1080 panel on a 2048 x 1280 grid, one frequency, ran in 1.9 s; here the panel is the slm element written with 8-bit grey levels (a phase-only lens-and-grating pattern from a callback); a Gaussian overfilling the panel passes the Gaussian power on the panel | budget 13 s (the S2 limit; 45 s when the CI variable is set, shared runners took 21-23 s); transmission 1e-3 of the erf product | +| `Slm_FullPanel_1920x1080_8Bit_RunsWithinTheScaleBudget` (slow) | S2 scale spike (docs/design/history.md): a 1920 x 1080 panel on a 2048 x 1280 grid, one frequency, ran in 1.9 s; here the panel is the slm element written with 8-bit grey levels (a phase-only lens-and-grating pattern from a callback); a Gaussian overfilling the panel passes the Gaussian power on the panel | budget 13 s (the S2 limit; 45 s when the CI variable is set, shared runners took 21-23 s); transmission 1e-3 of the erf product | | `Slm_GreyLevelBlaze_FirstOrderEfficiency_FollowsSincOfOneOverN` | N-level blazed grating written with grey levels through the slm element: first-order efficiency of the sampled staircase (4 samples per pixel) and the continuum formula [sin(pi/N) / (pi/N)]^2 (Goodman, Fourier optics, sec. 5.2) | 0.6 % of the efficiency (sampled staircase), 2 % (continuum) | | `Slm_NearestLevel_UsesTheCircularDistance_WhenTheCurveExceedsTwoPi` | nearest grey level when the curve spans more than 2 pi (0 .. 2.5 pi in 100 steps): images of the target modulo 2 pi inside the range are all candidates (circular distance) | exact | | `Slm_PhaseTarget_IsMappedToTheNearestGreyLevel_OfANonlinearCurve` | phase-target input: the target is wrapped to 2 pi and mapped to the nearest grey level of the (here S-shaped) curve; the result is the same as writing those grey levels directly | 1e-12 (identical fields) | | `Slm_WrappedRamp_AtOtherWavelengths_FollowsTheLiquidCrystalDispersion` | liquid-crystal dispersion: the 2 pi reset written at the design wavelength is 2 pi s(lambda) at lambda, s = lambda_d dn(lambda) / (lambda dn(lambda_d)); the first order of the wrapped ramp is the sampled Fourier coefficient with the phases scaled by s | 1.5 % of the efficiency | +| `SpectralShaper_GratingDispersion_MatchesLittrowReference` | Littrow condition (Hecht, Optics): for sin(theta) = m lambda N / 2 a grating in the focal plane of an f lens gives dx/dlambda = 2 f tan(theta) / lambda; the shaper built from the grating data equals the one given that dispersion | output power identical to 1e-12 of its peak | | `SpectralShaper_LinearPixelPhase_DelaysThePulse` | shift theorem on the pixelated mask: pixels carrying the linear phase tau (omega_j - omega0) delay the pulse by tau (the main pulse of a staircase; Weiner) | centroid of the main pulse within 2 fs of tau (the replica comb is outside the window of the centroid) | | `SpectralShaper_OffsetMovesTheArray_AwayFromTheSpectrum` | the mask array moves with offsetM: with unit amplitude on every pixel and outsideTransmission 0, an array centred on the spectrum passes all the energy (the pulse is far narrower than the array), the same array shifted by 1.5 array widths passes none | pass 1e-4, blocked 1e-6 | | `SpectralShaper_PixelGaps_MakeSidePulses_AtTwoPiOverThePixelBandwidth` | Weiner (Rev. Sci. Instrum. 71, 1929 (2000)): a Fourier-plane mask with pixel gaps (150 fs pulse: its bandwidth is narrow enough for the pixel width in angular frequency to be constant to 0.4 %, the map is exact in wavelength) is a comb in frequency of period d_omega (one pixel); active fraction f gives side pulses at +-2 pi / d_omega with energy [sin(pi f) / (pi f)]^2 of the main pulse; a Gaussian spot of 1/e^2 radius w0 multiplies the first harmonic by exp(-(pi w0 / pitch)^2) | 1 % of the side-pulse energy ratio, 1 fs position | | `StrokeAndDriveBits_ClipAndQuantiseCommands` | stroke limit and drive resolution: commands clip at +-stroke; a b-bit drive over +-stroke has 2^b - 1 steps | exact (1e-12) | | `TiltRamp_OnActuators_MovesTheFocus_ByFocalLengthTimesWavefrontTilt` | influence function: the surface at a neighbouring actuator equals the stated coupling (definition), and a tilt ramp on the actuators gives a wavefront tilt 2 cos(aoi) dh/dx that moves the focus of a lens by f theta (geometric optics) | 2 % of the shift (the lattice sum of the influence functions sets dh/dx, computed from the same functions) | +| `UnknownElement_NamesTheAoPlugin` | the unknown-element message names the ao plugin for its four types | message text | + +## AseStressTests + +Inter-pulse ASE (both directions, spectrally resolved) and the stress-birefringence pattern. + +| test | reference | tolerance | +|---|---|---| +| `AseModel_OnAnAmplifier_ReportsThePedestal_AndTheBudgetCloses` | the element reports the ASE leaving the signal mode and the depletion; the energy budget of the pulse still closes | 1e-6 | +| `Ase_CwSteadyState_ClosesThePhotonBalance_AndDepletesTheInversion` | photon balance in the cw steady state: net absorbed pump photons = N2 V / tau + stimulated ASE photons | 0.5 % | +| `Ase_Depletion_VanishesWithTheSolidAngle_AndGrowsWithIt` | limits: no solid angle gives no depletion; the depletion grows with the solid angle | 1e-6 | +| `Ase_NonUniformInversion_MatchesADirectIntegration_AndForwardDiffersFromBackward` | dI/dz = gamma(z) I + S(z) integrated with a fine RK4 for a linear inversion profile | 1e-6 (the model holds N2 constant in each of 400 slices, the reference is continuous: O(1/nz^2)) | +| `Ase_SliceStep_IntegratesTheSaturatedAmplifierLaw` | homogeneously saturated amplifier, dP/dz = g0 P / (1 + P / P_sat): ln(G / G0) + (G - 1) P_in / P_sat = 0 (integrated law; Siegman, Lasers, ch. 7; Saleh and Teich, Fundamentals of Photonics, ch. 15) | 1e-4 | +| `Ase_SpontaneousSource_IntoFourPi_IsTheRadiativeDecay` | Fuchtbauer-Ladenburg: 1 / tau_rad = 8 pi n^2 integral sigma_e(nu) nu^2 / c^2 dnu (Aull and Jenssen, IEEE JQE 18, 925 (1982)); the source into 4 pi, both polarisations, is N2 / tau_rad | 1e-9 (quadrature of the same integrals) | +| `Ase_UniformInversion_IsNspHnuDnuGMinusOne_BothEnds_BothPolarisations` | single-mode ASE per polarisation P = n_sp h nu dnu (G - 1), n_sp = sigma_e N2 / (sigma_e N2 - sigma_a N1) (Desurvire, Erbium-doped fiber amplifiers, 1994; Siegman, Lasers, ch. 6) | 1e-12 (uniform inversion: the slice step is exact) | +| `StressPattern_BeamAverage_EqualsTheLumpedDepolarisation_AndHasFourLobes` | Koechner, Solid-State Laser Engineering, ch. 7: the beam average of the four-lobed pattern, D = (1 - ) / 4, delta = C r^2 / r0^2 | 1e-6 (quadrature on the grid; the Gaussian is well inside the grid) | +| `StressPattern_InAGainElement_AgreesWithTheLumpedValue_AndTheProfileCarriesY` | the pattern on a cartesian grid carries the same beam-averaged loss as the lumped depolarisation reported by the element | 5 % at the exit radius (the gain is not uniform over the beam) | ## BeamSourceTests @@ -107,8 +132,10 @@ Beam sources and masks: Hermite-Gauss, Laguerre-Gauss, Bessel, Airy, measured ne | `FileMask_AppliesAmplitudeAndPhase` | T = amplitude^2 for a uniform map; a thin plate's phase scales with frequency | -- | | `HermiteGauss_RadiiFollowTheModeOrders_AtTheWaistAndAfterARayleighRange` | Hermite-Gauss modes: second-moment radius w(z) sqrt(2m + 1) (Siegman, Lasers, ch. 16) | -- | | `LaguerreGauss_VortexKeepsItsDarkAxis_AndItsRingScalesWithTheBeam` | Laguerre-Gauss modes: second-moment radius w sqrt(2p + \|l\| + 1), helical phase keeps a dark axis, ring at w sqrt(\|l\| / 2) | -- | +| `MaskFile_OfOneRow_IsConstantAlongY` | a mask file of one row is constant along y: a transmission of 0.5 everywhere passes 0.25 of the energy on a grid with ny = 1 and ny = 4 | 1e-9 | | `Mask_WithoutAProfile_WarnsAndPassesThePulse` | -- | -- | | `MeasuredNearField_FromAnNpz_PropagatesLikeTheBeamItSampled` | Gaussian beam w(z) from a sampled near field; known background subtracted | -- | +| `MeasuredScene_OfOneRow_IsConstantAlongY_OnGridsWithAnyNy` | a measured scene of one row (what a '1-D' scene saved at y = (j - ny/2) dx with ny = 1 is: its row is at y = -dx/2) reproduces on a grid with ny = 1, 4 and 128 the same x profile as a full map: radius w of the Gaussian and w sqrt(2) after one Rayleigh length | radius 1 %, w(z_R) 2 % | | `SpecialFunctions_MatchTabulatedValues` | Abramowitz & Stegun table 10.11: Ai(0), Ai(1), Ai(-1), Ai(5.5), first zero; H_3, L_2^1 closed forms | 1e-8 | | `VortexPlate_ConservesEnergy_AndDarkensTheAxis_AndNeedsACartesianGrid` | Spiral phase plate: a vortex of charge l has zero amplitude on the axis at every z | -- | | `ZonePlate_FocusesAtItsFocalLength_AmplitudeAndBinaryPhase` | Fresnel zone plate: amplitude on the axis 2 N_open U0, intensity 4 N_open^2 I0; binary phase plate 16 N_open^2 I0 (Born & Wolf 8.8) | 5 % | @@ -120,14 +147,18 @@ Gaussian beam tracking: thermal lenses and spatio-temporal couplings. | test | reference | tolerance | |---|---|---| | `AngularDispersionOnTheProfile_TwoGratingsOnAGrid_GiveTheTrackersChirpAndTreacysGdd` | Treacy GDD and the tracker's spatial chirp, from a field propagated on a grid | 3 % | +| `Duct_ManyThinSlices_ConvergeToTheDuct_WithSecondOrderError` | slices of a graded-index medium: N thin lenses + gaps converge to the duct as 1/N² | 1e-3 at N = 40, ratio 4 for 2N | +| `Duct_Matrix_IsTheQuadraticIndexResult_AndTheThinLensLimit` | quadratic-index medium n(r) = n0 (1 - g² r²/2): ABCD cos(gL), sin(gL)/(n0 g) (Saleh & Teich, ch. 1) | 1e-12 | | `Grating_AngularDispersionAndPulseFrontTilt_FollowTheGratingEquationAndHebling` | grating equation; Hebling, Opt. Quantum Electron. 28, 1759 (1996) | 0.2 % | | `PrismPair_InAir_GivesTheNegativeDispersionOfItsAngularDispersion` | angular-dispersion GDD -k0 L (d theta / d omega)^2; Snell's law at the prism faces | 1 % | | `Prism_AngularDispersion_MatchesSnellsLawThroughBothFaces` | Snell's law at both prism faces (finite difference of n(λ)) | 0.5 % | | `PulseFrontTilt_FromSpatialChirpAndGdd_MatchesTheGaussianResult` | Akturk et al., Opt. Express 13, 8642 (2005): 2 φ2 E σ² / (w² + 2 E² σ²) | 0.5 % | +| `Regen_ThermalLens_OnTheTrackedBeam_EqualsTheSamePassesOfAMultipass` | N round trips of a regenerative amplifier = N x passesPerRoundTrip duct passes | 1e-12 | | `SpaceTime_OnA1DBeam_ShowsTheTrackedPulseFrontTilt` | cross-model: spaceTime from the tracked beam vs the element's pulse-front tilt | -- | | `SpatioTemporalSwitch_TrackReportsWithoutPhase_OffForgetsTheCouplings` | -- | -- | | `ThermalLens_MeasuredFocalLength_FocusesTheTrackedBeamLikeAThinLens` | Gaussian-beam thin-lens law (Self, Appl. Opt. 22, 658 (1983)) | 1e-12, 0.2 % | | `ThermalLens_SmallBeam_TrackedFocalLengthIsInnocenzis` | Innocenzi et al., Appl. Phys. Lett. 56, 1831 (1990): f = πK w_p² / (P_h dn/dT) | 5 % | +| `ThermalLens_ThickRod_TrackedBeamIsTheDuctResult` | ABCD matrix of a quadratic-index duct (gL = 0.6): not the thin lens at the centre | 1e-9 | | `ThermalLens_TrackedBeam_AgreesWithTheSpatialGainModel` | cross-model: the radial spatial gain model | 7 % (aberrated lens) | | `TwoGratings_GiveTreacysGdd_AndTheGratingPairsSpatialChirp` | Treacy grating pair (IEEE JQE 5, 454 (1969)), Kostenbauder ray-pulse matrices | 0.1 %, 0.2 % | | `TwoGratings_ReproduceTreacysThirdOrderDispersion` | Treacy, IEEE JQE 5, 454 (1969): TOD of the exact grating-pair phase | 0.5 % | @@ -177,6 +208,37 @@ Branched systems: ports, graph compiler, interference, loops. | `UnusedPorts_RaiseAlerts_AndBecomeVirtual_UnlessDeclared` | -- | -- | | `Validate_PrintsTheGraph` | -- | -- | +## CatalogueSnapshotTests + +Material catalogue: values pinned, provenance, user catalogues. + +| test | reference | tolerance | +|---|---|---| +| `Catalogue_ReproducesTheValuesPinnedBeforeTheMove` | the values of the catalogue files equal the old C# tables | 1e-15 relative | + +## CatalogueTests + +Material catalogue: values pinned, provenance, user catalogues. + +| test | reference | tolerance | +|---|---|---| +| `CatalogueFiles_HaveSourceStatusAndUnitsInTheirKeys` | -- | -- | +| `CiddorAir_AtStandardConditions_EqualsTheDatabaseFormula` | Ciddor, Appl. Opt. 35, 1566 (1996) as published on refractiveindex.info (other/air/Ciddor: standard air, 15 C, 101.325 kPa, dry, 450 ppm CO2): n - 1 = 0.05792105 / (238.0185 - s^2) + 0.00167917 / (57.362 - s^2) | 1e-12 | +| `Crystal_ListsItsUnverifiedValues_AndVerifiedIndicesStayQuiet` | -- | -- | +| `EveryBuiltInName_ResolvesToACatalogueEntry_AndBuilds` | -- | -- | +| `GainMedium_ListsItsCatalogueValues_AndNotTheOnesTheElementOverrides` | -- | -- | +| `LinearRun_ReportsOnlyTheDispersionItUsed` | -- | -- | +| `PublishedIndices_AreReproduced` | Malitson, JOSA 55, 1205 (1965): n_d = 1.45846 at 587.56 nm; Schott N-BK7 data sheet: n_d = 1.5168 at 587.56 nm; Dodge 1984 MgF2 n_o = 1.3777 at 589.3 nm | 2e-5 / 1e-4 / 1e-4 | +| `Run_ListsTheApproximateValuesItUsed_AndNotTheOnesTheUserOverrode` | -- | -- | +| `SolidIonization_PartialOverride_NamesWhatTheRunGaveAndWhatCameFromTheCatalogue` | -- | -- | +| `StrictMaterials_AcceptsAUserValueMarkedVerified` | -- | -- | +| `StrictMaterials_AllowList_AcceptsNamedValuesOnly` | -- | -- | +| `StrictMaterials_RefusesUnverifiedValues_ListsThem_AndAcceptsOverridesAndVerifiedOnes` | -- | -- | +| `Tgg_Dispersion_FollowsTheMeasuredTable` | Schlarb and Sugg, Phys. Status Solidi B 182, K91 (1994): measured n of TGG at 20 C (refractiveindex.info main/Tb3Ga5O12/Schlarb) | 1e-3 | +| `UserCatalogue_ReplacesAQuantityWithANotice_AndCanAddAMaterial` | -- | -- | +| `UserGainMedium_ThermalDefaults_AreCatalogueValues_ReportedUnlessBothAreGiven` | design: the generic defaults live in the catalogue with a status, and a run that uses one says so | exact | +| `VerifiedDispersionSets_RecordTheirCheck` | -- | -- | + ## CavityAndCoherenceTests Resonators, partial coherence, thermal stress birefringence, amplifier thermal lens. @@ -580,7 +642,13 @@ Photo-ionisation and plasma. | test | reference | tolerance | |---|---|---| | `Keldysh_MultiphotonOrderForFusedSilica` | Keldysh: K = ceil(U_g / ħω) | 8 % | +| `Plasma_Avalanche_DoesNotAbsorbTheFieldASecondTime` | Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007), eq. 6 and 15: the avalanche is fed by inverse-bremsstrahlung absorption, which the Drude imaginary part already is | 1e-4 | | `Plasma_ElectronDensityBlueshiftAndAbsorption` | ρ = ∫ W dt; Drude blue shift | 3 % | +| `Plasma_EnvelopeAgreesWithCarrierResolved_ForA10FsMidInfraredPulse` | cross-check: the envelope model against the carrier-resolved field model, same operator | blueshift 2 %, energy 1 %, density 1e-4 | +| `Plasma_FixedDensity_FollowsTheFrequencyResolvedDrudeIndex_InEnvelopeAndCarrierMode` | Drude response of free electrons: Δn(ω) = -ω_p² / (2 n0 ω (ω + iν)) (Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007)) | 1e-8 (solver tolerance 1e-11), both field models | +| `Plasma_FixedDensity_SpatialModel_AppliesTheSameDrudeFilter` | the same Drude filter in the radial spatial model (a uniform plasma commutes with diffraction): energy transmission | 1e-7 | +| `Plasma_FrequencyResolved_TakesTheDriftEnergyOfTheElectronsBornInTheField` | drift energy of electrons born at rest in a linearly polarised field: U_p = e² E0² / (4 m ω²) per electron (Corkum, Phys. Rev. Lett. 71, 1994 (1993)) | 2 % | +| `Plasma_FrequencyResolved_TendsToTheCarrierValue_ForNarrowSpectra` | narrow-band limit: the frequency-resolved operator tends to the carrier value as the bandwidth goes to zero; R(ω0) = 1 | 1e-15; difference halves with the bandwidth | | `Ppt_MultiphotonAndTunnelLimits` | PPT rate: multiphoton I^K and tunnelling (ADK) limits | 3 %, 15 % | | `SpecialFunctions_DawsonAndElliptic` | tabulated Dawson and elliptic-integral values | 1e-12, 1e-07, 1e-06 | | `Uppe_AgreesWithEnvelopeForParaxialBeams_AndPlasmaClampsTheFocus` (slow) | cross-check: the envelope model for paraxial beams | 1e-06, 1 % | @@ -707,7 +775,7 @@ Modulator: pixelated phase and amplitude devices (SLM, DM surface), quantisation |---|---|---| | `BadInput_IsRefusedWithAMessage` | validation: a map of the wrong shape, both phase and path, and a quantisation of one level are refused with a message | exact | | `CrosstalkKernel_BlursTheWrittenMap_ByDiscreteConvolution` | cross-talk kernel w applied separably to the written map: a single raised cell of phase 1 with w = [1, 2, 1] / 4 gives 1/4, 1/2, 1/4 (discrete convolution); the Gaussian sigma equals the explicit kernel of its weights | 1e-12 | -| `CwRun_EqualsThePulse_AtTheCarrier` | a continuous-wave source and a pulse at the carrier see the same (monochromatic) device: the cw focal fluence pattern equals that of a 1 ns pulse | 2e-5 of the peak: a 1 ns pulse has a 0.4 GHz bandwidth, so the optical-path phase differs from the carrier's by ~1e-6 of its value across the band | +| `CwRun_EqualsThePulse_AtTheCarrier` | a continuous-wave source and a pulse at the carrier see the same (monochromatic) device: the cw focal fluence pattern equals that of a 1 ns pulse | 1e-7 of the peak (measured 1.3e-8): a 1 ns pulse has a 0.4 GHz bandwidth, so the optical-path phase differs from the carrier's by ~1e-6 of its value across the band; the cw line sits on the requested carrier | | `EdgeCellsAndGaps_AreAreaAverages` | area average over a grid cell: a cell half on the array and half outside gets (t_cell + t_outside) / 2; gaps transmit gapTransmission, so a fill factor 1/2 with gap amplitude g passes 1/2 + g^2 / 2 of the power when every grid cell lies in a cell or a gap | 1e-12 edge cell, 1e-6 power | | `FillFactor_SetsTheTransmission_AndTheReplicaOrders` | comb of active cells (fill 3/4 of a 4-sample pitch, edges on grid-cell edges): transmission 3/4; discrete Fourier series c_0 = 3/4, \|c_1\| = 1/4, so the first replica / zero order = 1/9; replicas at lambda f / pitch | 1e-6 transmission, 1 % order ratio | | `FixedPhase_RampDeflectionScalesWithWavelength` | fixedPhase: the written phase does not scale with frequency, so a ramp k0 theta x deflects by theta lambda / lambda0 (diffractive / geometric-phase device) | 1 % | @@ -715,8 +783,8 @@ Modulator: pixelated phase and amplitude devices (SLM, DM surface), quantisation | `IndexRule_WithConstantDeltaN_IsTheOpdRule` | chromatic index with a constant index modulation dn: phase = phi omega dn / (omega_d dn) = phi omega / omega_d, the opd rule | 1e-12 of the peak | | `Maps_InlineFileAndCallback_AreIdentical` | the three ways to give a map (inline array, NPZ file, callback "@name" evaluated at the cell centres) are the same device: identical focal fields | 1e-12 | | `OffsetArray_ClipsTheBeam_LikeAnAperture` | arrays outside the device block the light: outsideTransmission = 0 and an array covering half the beam transmits the Gaussian power inside it | 0.5 % | -| `OneStoredFrequency_IsAtTheCarrier_AndTheCwPatternMatches` | one stored frequency (spectralSamples = 1) sits at the energy centroid of the spectrum (the carrier of a 1 ns Gaussian, within one grid bin); the cw source's single bin is snapped to its grid lattice, 1.2e-6 from the requested carrier (the run notes print it), so the monochromatic focal patterns agree to the pattern change of that offset | stored frequency within 1e-7 of the carrier; pattern 3e-6 of the peak | -| `PhaseAtDesign_IsTheOpdRule` | naming (plan-ao-digital-twin-2026-11.md): "phaseAtDesign" is a phase given at the design wavelength and scaled with omega, the opd rule | exact | +| `OneStoredFrequency_IsAtTheCarrier_AndTheCwPatternMatches` | one stored frequency (spectralSamples = 1) sits at the energy centroid of the spectrum (the carrier of a 1 ns Gaussian, within one grid bin); the cw source puts its single line on the requested carrier (the lattice is shifted; it was snapped, 1.2e-6 away, until 2.2.1), so the two monochromatic focal patterns are the same | stored frequencies within 1e-9 of the carrier; pattern 1e-9 of the peak (measured 1e-16) | +| `PhaseAtDesign_IsTheOpdRule` | naming (history.md): "phaseAtDesign" is a phase given at the design wavelength and scaled with omega, the opd rule | exact | | `PhaseRamp_MovesTheFocus_ByFocalLengthTimesAngle` | a pure phase tilt k theta x moves the focus of a lens by f theta (geometric optics, Goodman, Fourier optics ch. 5) | 1 % | | `QuadraticPhase_FocusesLikeAThinLens` | a quadratic phase -k x^2 / (2 f) is a thin lens: a Gaussian waist w at the device focuses at z = f to lambda f / (pi w) (Siegman, Lasers, ch. 17), as the lens element does | 2 % of the analytic radius, 1 % of the lens element's | | `Quantisation_WrapsTheTopHalfLevel_ToLevelZero` | a quantised continuous phase at a wavelength off the design one: the level just below 2 pi wraps to level 0, not to a full turn (which would differ off-design) | exact | @@ -935,7 +1003,7 @@ Large spatial runs: frequency streaming, profile retention, cylindrical lens, fu | test | reference | tolerance | |---|---|---| | `CylindricalLens_FocusesOneAxisOnly` | cylindrical lens (focalLengthXM = f, focalLengthYM = 1e30 m): x focuses to lambda f / (pi w), y diffracts freely to w sqrt(1 + (z / z_R)^2) with z_R = pi w^2 / lambda (Siegman, Lasers, ch. 17) | 2 % | -| `FullPanel_1920x1080_SingleFrequency_RunsWithinTheSpikeBudget` (slow) | S2 scale spike (docs/design/notes/spikes.md): a 1920 x 1080 single-frequency run (modulator + 0.2 m gap + beam profile) took 3.3 s and 1.05 GB peak on the owner's laptop; a Gaussian overfilling the panel passes the Gaussian power on the panel (erf in x and y) | budget 4x the spike time (13 s; 45 s when the CI variable is set); keepProfiles used holds at least one profile less than all; transmission 1e-3 | +| `FullPanel_1920x1080_SingleFrequency_RunsWithinTheSpikeBudget` (slow) | S2 scale spike (docs/design/history.md): a 1920 x 1080 single-frequency run (modulator + 0.2 m gap + beam profile) took 3.3 s and 1.05 GB peak on the owner's laptop; a Gaussian overfilling the panel passes the Gaussian power on the panel (erf in x and y) | budget 4x the spike time (13 s; 45 s when the CI variable is set); keepProfiles used holds at least one profile less than all; transmission 1e-3 | | `KeepProfilesUsed_DropsUnreadProfiles_AndChangesNoResult` | outputs.keepProfiles = "used" drops the profiles no instrument reads; the instruments and the output are unchanged | exact | | `OneStoredFrequency_IsTheCentroid_OrTheNearestLitBin` | one stored frequency: the bin nearest the energy centroid; for a two-colour spectrum with no light at the centroid, the nearest bin that carries light (review finding M1) | exact | | `PrefixCachedStudy_WithKeepProfilesUsed_EqualsTheUncachedStudy` | a prefix-cached study resumes from the nominal run's pulses, so with keepProfiles "used" the nominal run still keeps every profile: the cached sweep equals the uncached one (review finding H2, 2026-11-10) | 1e-12 | @@ -975,6 +1043,7 @@ Coherent tile source: N sub-beams with piston, tilt and delay; combined far fiel |---|---|---| | `BadTileSpec_IsRefused` | validation: per-tile arrays of the wrong length and missing sizes are refused | exact | | `DelayMismatch_CostsTheTemporalOverlap_OfTheTwoPulses` | delay mismatch of two in-phase Gaussian pulses (intensity FWHM T): the energy on axis is 2E + 2 Int A(t) A(t - tau) dt, so relative to tau = 0 it is (1 + C) / 2 with the field overlap C = exp(-ln2 (tau / T)^2) | 3 % | +| `FourTiles_OnAxisPistonLoss_EqualsABranchedBalancedTreeRecombinedAtOnePoint` | N = 4 coherent beams of equal power recombined at one point: a balanced tree of 50/50 splitters (a Mach-Zehnder of Mach-Zehnders) with piston phase phi_k in each arm returns \|sum exp(i phi_k)\|^2 / 16 of the input power at its bright port (all arms equal: the whole input), exactly; the tile source gives the same fraction of its in-phase on-axis fluence. A Branched system recombines at a point and so cannot give the array's far field: this is the on-axis value only | Branched 1e-6 of the formula (the carrier is snapped to the grid), tiles against Branched 1.5 % | | `InPhaseTiles_CombinePeakIntensity_ByN` | N equal, in-phase, coherent beams: on-axis far-field peak = N x the peak of one beam carrying all the energy (N^2 for equal power per beam); Goodman ch. 4 (array theorem) | 2 % | | `Layouts_HexRingAndExplicit_PlaceTheTiles` | geometry: a hex layout of r rings has 1 + 3 r (r + 1) tiles, each at distance pitch from its nearest neighbour; a ring of n tiles lies on radius R at angles 2 pi i / n; explicit positionsM are used as given | 1e-12 | | `PistonErrors_CostTheRealisedCoherenceSum` | piston errors: the on-axis peak of the realised phases is \|sum exp(i phi)\|^2 / N^2 of the in-phase peak (exact for the draw); its expectation is 1/N + (1 - 1/N) exp(-sigma^2) | 1.5 % | diff --git a/docs/releasing.md b/docs/releasing.md index 70276c5..f568151 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,4 +1,4 @@ -# Releasing: compiled packages without the source +# Releasing: compiled packages, release assets and the installer `build.sh` (macOS / Linux) and `build.bat` (Windows) compile UltrafastEngine and lay out release folders you can copy to another computer: the programs and libraries you choose, the plugins you choose, no source code. Both call @@ -18,7 +18,104 @@ build.bat --cli --python --plugins transform zemax --platform win64 x86 --zos-ap Requirements: the .NET 10 SDK and Python 3.9+ (only the standard library; the wheel is written directly). The first build for a platform other than this machine's downloads that platform's .NET runtime pack from nuget.org. -## Output +## A release is the whole repository, in pieces you can take separately + +A release on GitHub has three kinds of file: the **source archive** (the full repository), one **asset per component and platform** +(`cli-2.2.0-osx-arm64.tar.gz`, `python-2.2.0.tar.gz`, `examples.Branched-2.2.0.tar.gz` ...), and `manifest.json` (every asset with its size, +SHA-256, the platform and what it needs) with **`install.py`**. GitHub cannot serve part of an asset, so the pieces are separate assets; +for source folders `install.py` can instead use git's partial clone (below). Maintainers build the assets; users run `install.py`. + +### Components + +A name selects the component and all its children (`examples` is every `examples.`; `plugins` every plugin). `all` is everything built for +the platform **except** `media` and `python-native`, which must be named. Dependencies come along (shown by `--list`). + +| name | contents | needs | +|---|---|---| +| `cli` (per platform) | the `ultrafast` program, self-contained, in `bin/` | | +| `plugins.` (`fitting`, `zemax`, `gpu`, `ao`, `twin`, `hitran`) | the plugin folder in `bin/plugins/` | `cli`; `plugins.twin` also `plugins.fitting` | +| `python` | the hosted-engine wheel, `pip install`ed by the installer | .NET 10 runtime at run time | +| `python-native` (per platform) | the wheel with the NativeAOT engine (no plugins) | conflicts with `python`; counts as `python` for dependencies | +| `cpp` (per platform) | C API: header, native library, CMake example | | +| `csharp` | `UltrafastEngine.Core.dll` for net10.0 and net48, plugin assemblies | | +| `zemax` (win-x64) | the OpticStudio extension, built only where your ZOS-API is found | | +| `schemas`, `docs` | JSON schemas; the Markdown documentation (not examples, papers, design notes) | | +| `examples.`, `examples.shared` | `docs/examples//`; the helper files in `docs/examples/` that every group imports | `examples.shared`, `python` | +| `media` | the figures and animations (png, gif, svg) under `docs/examples` and `docs/papers` | | +| `tests`, `component-tests` | test sources and data (C#, Python), no binaries; `tests/components` | `python` | +| `papers` | `docs/papers` (replications; never the licensed PDFs) | | +| `source` | every tracked file of the repository | | + +A name deeper than a component picks part of it: `examples.Branched.pump-probe` installs that example, plus the files that sit directly in +`docs/examples/Branched/` (the group's helpers) and, through `examples.shared`, the shared ones. Source components use the repository paths +(`docs/examples/...`), so a release install and a git checkout give the same tree. + +### install.py + +Standard library only; it is in the repository and attached to every release. + +```bash +python install.py --list # components, sizes for this platform, what is installed +python install.py --include python tests examples.Branched --dest ~/uf # only these (and what they need) +python install.py --include examples.Branched.pump-probe --dest ~/uf # adds to the same folder +python install.py --include all --exclude media --version 2.2.0 +python install.py --include docs examples.Fibres --source git # source folders by git partial clone (see below) +python install.py --uninstall --include tests --dest ~/uf # remove part ... +python install.py --uninstall --dest ~/uf # ... or all +``` + +| option | meaning | +|---|---| +| `--include`, `--exclude` | component names; no `--include` is an error (the installer never guesses) | +| `--version X`, `--latest` | the release (default: latest); `--base-url` reads `manifest.json` and the assets from another folder or URL (`file://` too) | +| `--platform auto` | `win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, `osx-arm64`, `osx-x64`; a component without an asset for it is skipped under `all`, an error when you name it | +| `--dest DIR` | default `./UltrafastEngine` | +| `--source release\|git` | release assets (default), or `git clone --filter=blob:none --sparse --branch v` and a cone-mode `sparse-checkout` of the folders of the selected source components | +| `--no-pip`, `--python EXE` | wheels are installed with `EXE -m pip install --find-links /python ultrafast-engine[hosted]` (never from PyPI; the wheel's dependencies are); `--no-pip` only unpacks them | +| `--dry-run`, `--force` | show what would happen; overwrite or remove files even if changed | + +What it does: it downloads **only** the chosen assets and checks each SHA-256 before anything is written (a mismatch installs nothing), unpacks them, +and writes `/.ultrafast-install.json` with every file, its hash, the component and the version. Therefore: + +* **Uninstall removes exactly those files** and the folders it emptied; a file you changed is left in place with a warning, a file it did not install is never touched + (an existing file that is not in the record is not overwritten either, without `--force`). Removing a component that another installed one needs is refused. +* A second `--include` **adds** to an install; installing a newer `--version` of a component **upgrades** it (the old unmodified files are removed first). +* The ZOS-API is never installed, removed or packaged; an archive that holds one is refused. +* With `--source git` the tag pins the version. Cone mode brings the files in the parent folders of a selected folder (that is how one example gets its shared helpers), and + figures cannot be separated from their folders, so `media` has no effect there. Uninstall shrinks the sparse set; git keeps files with local changes. + Built components (`cli`, wheels, `cpp` ...) are not in git: ask for them from the release. + +### Building the assets + +`build/package.py` takes the same names; the old flags are aliases (`--cli` = `--components cli`, also `--python --cpp --csharp --zemax --examples --docs`). + +```bash +./build.sh --components cli python plugins schemas docs examples tests papers --platform linux-x64 osx-arm64 win-x64 +./build.sh --components cpp python-native --platform osx-arm64 # NativeAOT: on the target OS (the owner's Mac for macOS) +./build.sh --components all media source --exclude papers --dry-run # the plan only +python build/release_components.py merge dist/release-2.2.0/manifest.json dist/a/manifest.json dist/b/manifest.json # manifests of several jobs +python build/release_components.py check-zosapi dist/release-2.2.0 # fails if any archive or wheel holds a ZOSAPI file +``` + +Output: `dist/release-/` with `-[-].tar.gz` (reproducible: sorted, fixed timestamps), `manifest.json`, `install.py`. +Source components are built from `git ls-files`, so untracked files, results folders and the licensed paper PDFs can not get in. The writer refuses a +`ZOSAPI*` file (the tracked `ZosApiStubs` sources are ours and allowed), and the packager checks every archive and every wheel inside one again at the end. +Built components reuse the steps of the single-folder packages above, so a platform-independent component is built once. + +### The release workflow + +`.github/workflows/release.yml` runs on a `v*` tag (or by hand, which only builds and tests). Job `managed` builds everything that is plain files or pure .NET, and cross-publishes +the CLI for win-x64, linux-x64, linux-arm64, osx-arm64 and osx-x64; job `native` builds `cpp` and `python-native` on Linux x64 / arm64 and Windows and runs the C example; +job `publish` merges the manifests, runs the ZOS-API gate, installs a selection into a temporary folder (`cli`, `schemas`, `examples.Branched`), runs the CLI and an example +in strict mode, uninstalls and checks the folder is empty, installs the wheel in a fresh virtual environment (`ultrafast.devices()`, the quickstart), and attaches the assets +to a **draft** release. The NativeAOT components for macOS are built on the owner's Mac and added with `gh release upload`. **A tag is created only when the owner asks.** +The workflow has not run on GitHub yet. Rehearsed by hand on macOS: the asset build, the manifest merge, the ZOS-API gate, and the installer install / run / uninstall steps. Not run: the Windows and Linux NativeAOT builds, the CMake example, the wheel in a fresh environment, the upload (see [session-f](design/history.md)). + +## Single-folder packages (`--cli --python ...`) + +The original mode: one folder per platform with the components you choose, no source. + +### Output `dist/UltrafastEngine--/` plus `.zip` (Windows, portable) or `.tar.gz` (macOS, Linux, keeps the executable bits): @@ -64,8 +161,8 @@ package is still built. The selected plugins go into `bin/plugins/`, the hosted wheel (`ultrafast/bin/hosted/plugins/`) and `csharp/plugins/`. Users add others with `ultrafast plugins install ` (copies into -`~/.ultrafast/plugins`). The NativeAOT library (C API, native wheel) cannot load plugins at run time; the `gpu` -plugin is compiled into it when selected (`-p:UF_WITH_GPU=false` otherwise). The GPU back-ends load the vendor +`~/.ultrafast/plugins`). The NativeAOT library (C API, native wheel) cannot load plugins at run time; the `gpu`, +`hitran` and `ao` plugins are compiled into it when selected (`-p:UF_WITH_GPU=false`, `UF_WITH_HITRAN=false`, `UF_WITH_AO=false` otherwise; the `twin` plugin is Python plus a CLI command and needs nothing). The GPU back-ends load the vendor libraries (CUDA driver, NVRTC, OpenCL) from the target computer at run time; nothing NVIDIA-owned is packaged. ## The ZOS-API rule diff --git a/docs/roadmap.md b/docs/roadmap.md index 0824615..bb9da12 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -18,7 +18,7 @@ reproduced in [papers](papers/README.md). Priorities, most useful first. -**Done (November 2026, merged 2026-11-10):** adaptive optics, temporal shaping, conditioned transforms and a digital twin, in five sessions ([roadmap](design/roadmap-ao-twin-2026-11.md)). S1 (Python package structure, plotting, Zernike plot, animation, grouped examples, optical-computing examples, gallery), S2 (core primitives: the `modulator` element, coherent tiles, in-memory maps, frequency streaming), S3 (the `ao` plugin), S4 (fitting across conditions and the `twin` plugin) and S5 (AdaptiveOptics, DigitalTwin and Fibres examples, documentation, verification) are done. **Still to do from that plan:** the MCP server for agents, a component gallery and a complex-field-shaping example for the `ao` plugin, a spline-against-network twin example, a wavefront twin, `UF_WITH_AO` in the native library, a grid-planner hook for shaper side pulses, and the same figure-and-story treatment for the older examples. +**Done (November 2026, merged 2026-11-10):** adaptive optics, temporal shaping, conditioned transforms and a digital twin, in five sessions ([record](design/history.md); the next iteration is [planned](design/next-steps.md)). S1 (Python package structure, plotting, Zernike plot, animation, grouped examples, optical-computing examples, gallery), S2 (core primitives: the `modulator` element, coherent tiles, in-memory maps, frequency streaming), S3 (the `ao` plugin), S4 (fitting across conditions and the `twin` plugin) and S5 (AdaptiveOptics, DigitalTwin and Fibres examples, documentation, verification) are done. **Still to do from that plan:** the MCP server for agents, a component gallery and a complex-field-shaping example for the `ao` plugin, a spline-against-network twin example, a wavefront twin, `UF_WITH_AO` in the native library, a grid-planner hook for shaper side pulses, and the same figure-and-story treatment for the older examples. 1. **Design plugins** (separate plugins; each emits ordinary `.ufs` objects that the core re-checks) - *Coating design*: analysis of R, T, group delay and GDD over band, angle and polarisation, and the field inside a diff --git a/docs/testing.md b/docs/testing.md index c248ef2..b1276a4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -43,11 +43,12 @@ Framework 4.8/Mono). In Visual Studio set it as start-up project and press Ctrl+ | sampling, measured data, fit | near/far-field pinhole and slit transmissions vs Gaussian analytics, off-axis pinhole, far-field fibre colour shift on a 1-D beam; CSV/NPZ loading and units; zero residual when data = simulation (curve, d-scan trace, values); normalisations and background; missing files become warnings; pseudo-inverse null space; fit paths (lists, overrides); fit recovers energy and chirp from a spectrum and autocorrelation; unidentifiable parameter flagged; Python comparison, fit and figures | 2 % / 1 % / exact | | grids | exact resampling round trip between derived grids; carrier change SHG → glass; chirped input fits the auto window | 1e-9 | | components | Gaussian beam tracking (lens, off-axis mirrors), Zernike / Strehl, plate and end-face ghosts (delay, level, Kerr pre-pulse), ND OD(λ), thick and compound waveplates, TFP Tp / Ts, Faraday rotation and dispersion, GDD ripple, pedestals | 10⁻⁴ ... 2 % | -| gases, ionisation | rotational impulse response and revivals, molecular gas data, electronic + rotational n2, PPT / ADK / Keldysh rates and limits, Dawson and elliptic functions, electron density, plasma blue shift, filament clamping (UPPE vs envelope) | 10⁻⁹ ... 5 % | +| gases, ionisation | rotational impulse response and revivals, molecular gas data, electronic + rotational n2, PPT / ADK / Keldysh rates and limits, Dawson and elliptic functions, electron density, plasma blue shift, filament clamping (UPPE vs envelope); the **Drude** filter exp(i (ω/c) Δn(ω) L) of a prepared plasma in the envelope, carrier-resolved and radial models (1e-8, 15 rad), its narrow-band limit, the drift energy U_p of electrons born in the field (2 %), envelope vs carrier-resolved with ionisation (2 %, 1 %) | 10⁻⁹ ... 5 % | | fibres | fundamental soliton over 5 L_D, D(λ) = Taylor, loss / gain / gain goal, tapered γ(z) B-integral, Raman kernels (unit integral, causality, Hollenbeck peak 13.2 THz), dispersive-wave phase matching, quantum-noise energy, coherence of a soliton and of a long-pulse supercontinuum | 10⁻³ ... 3 % | | instruments (device) | spectrometer normalisation modes, settings echo, FROG acceptance (product) and resolution (convolution), AC resolution and acceptance, detector calibration / noise / bits reproducibility, filter in front, polarimeter extinction, every enum value accepted | 10⁻¹² ... 5 % | | speed, studies, retrieval | mixed-radix / Bluestein FFT vs DFT, SIMD = scalar, scans with and without the prefix cache, optimisation, elasticities, Sobol indices; PCGPA, ePIE and d-scan retrieval of chirped pulses | 10⁻¹² ... 5 % | | transforms, fitting | transform phase == dispersion element; filter, energy and loss budget; replica level and grid room; Jones rotation / quarter wave; minimum phase is causal; not-a-knot spline exact for parabolas; every operator inverts; wavefront on a radial profile and 1-D note; ASE pedestal level; schema accepts fit objects | 10⁻¹² ... 2 % | +| ASE and stress birefringence (`AseStressTests`) | uniform-inversion ASE = n_sp h ν Δν (G − 1) at both ends, one and two polarisations; non-uniform inversion vs RK4; spontaneous source into 4π = N2 ⟨hν⟩ / τ_rad (Füchtbauer-Ladenburg); slice step vs the saturated-amplifier law; cw photon balance with ASE; depletion vanishes with the solid angle; element reports and the pulse budget closes; stress map unitary, beam average = lumped D, four lobes with alternating sign; pattern in a gain element and its carried y profile | 10⁻¹³ ... 5 % | | backward propagation | linear chain (glass, transform, dispersion) recovers a 60 fs input; nonlinear plate (B > 2) un-broadens to the input bandwidth and peak power; active elements refused / identity / linearised | 1 % | | fits | marked transform parameters from a spectrum (layers, influence, SHA-256 provenance, fitted system on a new input); GDD and TOD from a d-scan; prior-only parameter (σ = prior, influence 100 % priors); instrument resolution as a parameter; CMA-ES from a far start; MCMC 95 % intervals contain the truth; smooth spline phase from a FROG | 0.01 ... 5 % | | resolution and sampling | `axis` explicit pixels with pixel bins; axis resolution == spectrometer resolution; coarse AC delay steps; dB curves follow their linear curves; input-spectrum deconvolution recovers 2 nm from a 2.83 nm recording; Gaussian tails restore a truncated spectrum; measured-resolution mismatch reported (coarser simulated instrument) or matched (coarser recording) | 1 ... 6 % | @@ -64,18 +65,18 @@ Framework 4.8/Mono). In Visual Studio set it as start-up project and press Ctrl+ | regression cases (`OpenBugRegressionTests`) | tilted plate GDD = refracted-colour k_z(ω)·d (tracked and untracked, 1e-5); fed beams at a Sequential splitter keep their carrier and are booked when outside the grid; a spectrum outside the band is an error; probe arrays per packet; leakMirror GDD about the beam's own carrier; reported GDD at the requested carrier (1000 fs² with 2000 fs³ TOD reports 1000 fs²); critical power constant | 1e-8 ... 1e-3 | | measured pedestal (`ComponentTests.Pedestal_Measured_FollowsTheRecordedTrace`) | the pulse's intensity follows the recorded trace on average (csv in dB and arrays; excluded rows and beyond-trace zero) | 25 % (random field) | | logo generator (`python/tests/test_logo.py`, `assets/logo/make_logo.py`) | deterministic output; flat coefficient cells below 96 px and Zernike maps above; Zernike modes 2-10 rms-normalised over the disc (Noll 1976); text to the right of the mark, lighter text on the dark-page variant; SVGs are valid XML and the text fits the canvas; the long logo has five tiles then one line of text inside the canvas; the docs copies and favicon are written; the ICO holds seven sizes as PNG | exact; 2·10⁻³ (normalisation) | -| ao devices (`AoDeviceTests`, 20; 1 slow) | deformable mirror tilt, 2 cos(aoi), neighbour coupling, stroke, hex layout; SLM blaze efficiency vs levels `[sin(π/N)/(π/N)]²`, binary-grating orders, liquid-crystal dispersion, nearest grey level, 1920 x 1080; AOPDF delay, GDD (Diels and Rudolph), window, replica; spectral shaper side pulses `[sin(πf)/(πf)]² exp(-2(π w0/p)²)` (Weiner 2000) | 0.6 % ... 1e-9 | -| ao algorithms and plugin (`python/tests/test_ao_algorithms.py`, `test_ao_plugin.py`) | GS error monotone (Fienup 1982), WGS uniformity, Bolduc and double-phase encoding, Shack-Hartmann spot shift f dW/dx, Southwell reconstruction; Strehl against Maréchal exp(-σ²), correction and loop `(1 - g)^k`, modal search, GS hologram through the `slm` element (correlation 0.9995), tiled array N, piston `|Σe^{iφ}|²/N²`, delay overlap, SPGD locking, transmission matrix focusing `(π/4)(N-1)+1` (Vellekoop and Mosk 2007), closed-loop GDD shaping | 0.5 % ... 1e-9 | +| ao devices (`AoDeviceTests`, 26; 1 slow) | deformable mirror tilt, 2 cos(aoi), neighbour coupling, stroke, hex layout; SLM blaze efficiency vs levels `[sin(π/N)/(π/N)]²`, binary-grating orders, liquid-crystal dispersion, nearest grey level, 1920 x 1080; AOPDF delay, GDD (Diels and Rudolph), window, replica; spectral shaper side pulses `[sin(πf)/(πf)]² exp(-2(π w0/p)²)` (Weiner 2000), grating dispersion at Littrow, grid-planner room and wrap warning for shapers | 0.6 % ... 1e-9 | +| ao algorithms and plugin (`python/tests/test_ao_algorithms.py`, `test_ao_plugin.py`) | GS error monotone (Fienup 1982), WGS uniformity, Bolduc and double-phase encoding, Shack-Hartmann spot shift f dW/dx, Southwell reconstruction; Strehl against Maréchal exp(-σ²), correction and loop `(1 - g)^k`, modal search, GS hologram through the `slm` element (correlation 0.9995), tiled array N, piston `|Σe^{iφ}|²/N²`, delay overlap, SPGD locking, transmission matrix focusing `(π/4)(N-1)+1` (Vellekoop and Mosk 2007), closed-loop GDD shaping, Bolduc and double-phase maps through the `slm` element with a varying complex field (first-order energy 1.1 % and 0.5 %, correlation 0.99 and 0.99998), tiled-array lobe fractions against the array factor (2e-5), SLM pixels on the grid points (a blaze keeps `cos²(π/P)` of its light half a pixel off, all of it with the offset, 1e-6) and the warning, identical tiles reaching the axis alike only when the lens is resolved, `packaging` flags and the native `ao` listing | 0.5 % ... 1e-9 | | fitting across conditions (`FittingConditionsTests`, 15) | shared parameters from two conditions (1e-3); held-out predicted within the noise, and flagged when the model cannot predict it; per-condition resolutions; nonlinear-phase slope from five energies (1e-3); every dependence kind against its formula (1e-12); a smooth surface; network constraints hold exactly for 50 random weight sets | 1e-12 ... 3 % | | twin plugin (`python/tests/test_twin.py`, 9; `PluginTests`) | the fit recovers the hidden centre, width and slope of the synthetic lab; held-out energies inside and outside the training range predicted (chi² per point about 1); a saturating lab is flagged `fits-training-only`; stored dependences reproduce the fitted values (1e-9); the planner ranks the placements and a zero-energy shot cannot see the slope; paired what-if; the drift tracker follows a 0.5 nm shift; benchmark 3 of 3; goal files; `ultrafast twin` registered and a missing interpreter exits with 2 | 1e-9 ... 3σ | | branched-system plot (`python/tests/test_plot_graph.py`) | layers and lanes of a Mach-Zehnder graph; the system figure draws every element | exact | -| components (`Components/`: `ComponentSheet`, `LinearComponentTests`, `InstrumentCrystalComponentTests`) | component sheet at 515 nm / 1030 nm / 2 µm and 1 ps / 10 fs: energy and loss budget, reported GDD vs analytic (Sellmeier × Ciddor, Treacy), bandwidth, Sequential = Branched (1e-12); leakMirror GDD per port; fed beam of another colour keeps its carrier; sech² AC ratio 1.5427, TOCC √(3/2), SHG-FROG marginal; Python scripts with plots (10 families, 1691 checks, 24 KNOWN engine disagreements): [Component tests](component-tests.md) | 1e-12 ... 1e-3 | +| components (`Components/`: `ComponentSheet`, `LinearComponentTests`, `InstrumentCrystalComponentTests`) | component sheet at 515 nm / 1030 nm / 2 µm and 1 ps / 10 fs: energy and loss budget, reported GDD vs analytic (Sellmeier × Ciddor, Treacy), bandwidth, Sequential = Branched (1e-12); leakMirror GDD per port; fed beam of another colour keeps its carrier; sech² AC ratio 1.5427, TOCC √(3/2), SHG-FROG marginal; Python scripts with plots (10 families, 1694 checks, 24 KNOWN engine disagreements): [Component tests](component-tests.md) | 1e-12 ... 1e-3 | | crystal chains (`CrystalChainTests`) | SHG then SFG: **Manley-Rowe** photon balance, main and fed beams swapped, 31.3° THG cut of BBO at 1064 nm (Dmitriev), cross-correlation FWHM of Gaussians; efficiency as L² then L past τ / GVM; SH bandwidth 0.886 / (GVM L); tilted arm warns, non-collinear `sfg` refused | 1e-3; 0.3°; 1 ... 3 % | | diffraction by apertures (`DiffractionTests`) | plane wave through a round iris: on-axis **4 F sin²(πN/2)** (Born & Wolf 8.8) at N = 3, 2, 1; Gaussian and flat-top through an iris vs the **Fresnel integral** over the whole profile and transmission 1 − exp(−2a²/w²); square flat-top through a square aperture vs the separable Fresnel integral (512²); far zone: first Airy ring 0.61 λz/a, first sinc² zero λz/2a; area-averaged hard edges on coarse and fine grids, warning under 10 samples; absorbing boundary (wrapped light removed, loss budget closes); linear profile-only path = full-field path; square shapes need a cartesian grid; `test_double_slit_matches_the_fresnel_integral`: a Gaussian on two slits vs a direct sum of the Fresnel kernel | 1 ... 2 % of the peak (observed 0.02 ... 0.7 %); 10⁻⁴ (paths) | -| modulator (`ModulatorTests`) | tilt moves the focus by f θ; a quadratic phase focuses like a thin lens; N-level blaze efficiency [sin(π/N)/(π/N)]² (also the exact sampled staircase); fill-factor transmission and replica orders; gaps and edge cells are area averages; rotation; wrapped ramp = grating (x₁ ∝ λ) and unwrapped ramp = prism; `fixedPhase`; `index` rule (constant Δn = `opd`; = `fixedPhase` × α; blaze efficiency at depth α); cross-talk kernel convolution; inline / file / callback maps identical and N loop trials = N file runs (1e-12); cw = narrow pulse; one stored frequency at the carrier; sampling-error law 1/K², warning, and bound on the true error; zero-order leak; quantisation wrap; wavefront-sensor slope maps (tilt, defocus) | 1e-12 ... 2 % | -| coherent tiles (`TileSourceTests`) | N in-phase tiles give N × the single-beam peak; grating lobes at λ f / pitch; piston errors give \|Σ e^{iφ}\|² / N²; delay mismatch (1 + e^{−ln2 (τ/T)²}) / 2; hex / ring / explicit layouts; tilted tile focuses at F θ; tophat slit far field; unequal amplitudes; delay sign | exact ... 3 % | +| modulator (`ModulatorTests`) | tilt moves the focus by f θ; a quadratic phase focuses like a thin lens; N-level blaze efficiency [sin(π/N)/(π/N)]² (also the exact sampled staircase); fill-factor transmission and replica orders; gaps and edge cells are area averages; rotation; wrapped ramp = grating (x₁ ∝ λ) and unwrapped ramp = prism; `fixedPhase`; `index` rule (constant Δn = `opd`; = `fixedPhase` × α; blaze efficiency at depth α); cross-talk kernel convolution; inline / file / callback maps identical and N loop trials = N file runs (1e-12); cw = narrow pulse; one stored frequency at the carrier; sampling-error law 1/K², warning, and bound on the true error; zero-order leak; quantisation wrap; wavefront-sensor slope maps (tilt, defocus) | 1e-12 ... 2 %; a cw line on the requested carrier equals the one-stored-frequency pulse (1e-9), snapped lattice only when `snapCarrier` is written | +| coherent tiles (`TileSourceTests`) | N in-phase tiles give N × the single-beam peak; grating lobes at λ f / pitch; piston errors give \|Σ e^{iφ}\|² / N²; delay mismatch (1 + e^{−ln2 (τ/T)²}) / 2; hex / ring / explicit layouts; tilted tile focuses at F θ; tophat slit far field; unequal amplitudes; delay sign | exact ... 3 %; four tiles with pistons against a Branched balanced tree recombined at one point (1e-6 against the formula, 1.5 % tile source) | | large spatial runs (`SpatialStreamingTests`) | streaming over stored frequencies (`frequencyBlock`) = all at once (1e-12); `keepProfiles: "used"` drops unread profiles with identical results, also in a prefix-cached study; one stored frequency is the centroid (or the nearest lit bin); cylindrical lens focuses one axis (λ f/π w) and leaves the other to diffract; slow: a 1920 × 1080 cw panel within 4 × the spike's time, its Gaussian power on the panel, and at least one profile less held with `used` | 1e-12 ... 2 % | -| beam sources and masks (`BeamSourceTests`) | Airy function and polynomials vs tables; Hermite-Gauss and Laguerre-Gauss radii, dark vortex axis, LG node; **Durnin** Bessel core over 6 Rayleigh ranges; **Siviloglou & Christodoulides** Airy trajectory; measured near field; zone plate 4 N² / 16 N² on the axis; axicon core; **Young's** fringes; vortex plate; file mask | 1e-12 ... 5 % | +| beam sources and masks (`BeamSourceTests`) | Airy function and polynomials vs tables; Hermite-Gauss and Laguerre-Gauss radii, dark vortex axis, LG node; **Durnin** Bessel core over 6 Rayleigh ranges; **Siviloglou & Christodoulides** Airy trajectory; measured near field; zone plate 4 N² / 16 N² on the axis; axicon core; **Young's** fringes; vortex plate; file mask | 1e-12 ... 5 %; one-row measured scenes and mask files on grids with ny = 1, 4, 128 | | cw, nonlinear boundary, retrieval (`FieldToolsTests`) | cw through an iris: Fresnel intensity; Kerr phase of a cw beam; absorbing layer in the stepper vs the linear path; **Gerchberg-Saxton** on a known phase; phase map round trip; PNG near field with background, centre, threshold | 1e-3 ... 3 % | | cavities and coherence (`CavityAndCoherenceTests`) | **Kogelnik & Li** eigenmode; two-mirror formulas; Fox-Li vs ABCD; plane-mirror loss **N^(−3/2)** (Fox & Li); **Gaussian-Schell** M² law; **Koechner** depolarisation and bifocal lens; amplifier lens per pass | 1e-3 ... 25 % | | branched systems (`BranchingTests`) | carrier phase of glass; **Mach-Zehnder** fringes, spectral fringes pinning the sign; separate packets; **Airy function**; ring pulse train; steady-state loop; every graph check, port form and alert; virtual beams; branch-fed crystal against `aux`; modelled autocorrelator **√2 τ** | 1e-9 ... 1 % | @@ -83,12 +84,15 @@ Framework 4.8/Mono). In Visual Studio set it as start-up project and press Ctrl+ | splitter family and space (`BranchedElementsTests`) | **Gouy phase**; **Michelson** fringes; **Malus**; leak, dichroic; **Gaussian overlap** in tilt and offset; tilt fringes **λ/θ** of carried profiles; harmonic phase; crystal ports; tail through a splitter; **f-2f** following the CEP | 1e-9 ... 3 % | | modelled instruments (`ModelledInstrumentsTests`) | **IAC 8:1**; **FTS** (Wiener-Khinchin) against the input; FROG marginal **√2 τ**; study node cache and array outputs | 0.2 ... 2 % | | review regressions (`ReviewRegressionTests`) | **M²** of tilted, decentred, Hermite-Gauss and Laguerre-Gauss beams (2m + 1, 2p + \|l\| + 1) on each axis; centroid radius; packets at `end`; packets at crystal ports; passive `steadyState`; all-pass ring | -| beam tracking (`BeamTrackingTests`) | thermal lens on the tracked beam: small beam = **Innocenzi** πKw_p²/(P_h dn/dT); a measured focal length focuses like a thin lens (Gaussian lens law); tracked radius vs the radial spatial gain model (7 %, aberrated lens). Spatio-temporal couplings (**Kostenbauder**): grating angular dispersion and **Hebling** pulse-front tilt; grating + gap + grating = `gratingPair` (**Treacy** GDD, spatial chirp); pulse-front tilt from spatial chirp and GDD vs the Gaussian result (Akturk 2005); prism vs Snell at both faces; 4f zero-dispersion stretcher GDD = 0; `physics.spatioTemporal` modes | 0.1 ... 7 % | +| beam tracking (`BeamTrackingTests`) | thermal lens on the tracked beam: small beam = **Innocenzi** πKw_p²/(P_h dn/dT); the thermal lens as a graded-index duct (ABCD cos gL, sin gL / n0 g, thin-lens and hyperbolic limits 1e-12; N thin slices converge as 1/N²; a thick rod against the exact duct 1e-9); a measured effective focal length; N regenerative round trips = N multipass passes (1e-12); tracked radius vs the radial spatial gain model (7 %, aberrated lens). Spatio-temporal couplings (**Kostenbauder**): grating angular dispersion and **Hebling** pulse-front tilt; grating + gap + grating = `gratingPair` (**Treacy** GDD, spatial chirp); pulse-front tilt from spatial chirp and GDD vs the Gaussian result (Akturk 2005); prism vs Snell at both faces; 4f zero-dispersion stretcher GDD = 0; `physics.spatioTemporal` modes | 0.1 ... 7 % | +| material catalogue (`CatalogueSnapshotTests`, `CatalogueTests`) | every built-in index, n2, Raman kernel, rotational line and ionisation datum pinned to 1e-15 (the move of the tables into catalogue files was a pure refactor); catalogue files lint (source and status on every quantity, an uncertainty on every unverified one, units in the key names); verified dispersion sets record their comparison with the database page; published indices (Malitson fused silica n_d = 1.45846, Schott N-BK7 n_d = 1.5168, Dodge MgF2); a user catalogue replaces a quantity with a notice and can add a material; a run lists exactly the approximate / unverified values it used and not the ones the user overrode; gain media and crystals report theirs (thermal data only with a thermal lens, a given deff or doping replaces the catalogue's); Ciddor standard air equals the database formula (1e-12); `physics.strictMaterials` / `ULTRAFAST_STRICT_MATERIALS` refuse unverified values and accept overrides and user-verified ones | 1e-15 ... 1e-4 | | delayed light, follow-ups (`DelayedLightTests`) | smooth elements skip the exact long-grid step with the same result (energy 1e-9, power 1e-6) and are counted; first-order cross-phase of the tail vs the full nonlinear difference field (B = 0.11: 7.5 %); analytic surface scatter: in-mode share = numerical integral of the roughness spectrum, lost power TIS (1 - g), lambda^-2, returned power g TIS, plate has 2 surfaces; detector spectral response = table, photon counting = x lambda, presets (run spec name, file, overrides, unknown name) | 1e-12 .. 15 % | | HITRAN, partition sums and real lines (`HitranTests`) | partition table interpolation and range check; strength follows the table (T^-3 vs T^-1.5); `.partition.csv` found next to the lines; opt-in **real H2O, CO2, O2 lines at 250 / 296 / 350 K vs HAPI** (`ULTRAFAST_HITRAN_VALIDATE_DIR`, `plugins/hitran/tools/make_hapi_reference.py`) | 1 % integrated, 4 % worst point | +| release assets and installer (`test_release.py`) | component resolution (dotted names, dependencies, excludes, `all` without `media`, platform choice, conflicts); `install.py` against a local release: select, add, upgrade, uninstall leaves nothing, a changed file is kept, a file it did not install is not overwritten, hash mismatch installs nothing, unsafe paths and ZOS-API archives refused; the ZOS-API gate finds a planted file in an archive and in a wheel inside one; manifest merge; the git sparse checkout and uninstall in a temporary repository; the packager writes and installs real source assets | exact | | release packaging | `build/package.py`: platform aliases, plugin selection, ZOS-API assemblies never copied, the wheel writer (metadata, extras, entry point, RECORD hashes), dry run (Python `test_packaging.py`) | exact | | cross-code | N ~ 2.4 higher-order soliton with TOD and self-steepening vs **pyNLO**: bulk medium and `fiber` element, no Raman, Blow-Wood Raman, Lin-Agrawal Raman (pyNLO's weights), flat gain | < 0.3 % (gain 0.5 %) | | cross-code | spatial engine vs **LightPipes** (`test_lightpipes.py`, skipped without it): fluence maps through free space, lenses, cylinder, curved mirror, tilt, decentre, HG, eight Zernike terms, vortex handedness, axicon, apertures, double slit, Talbot, glass; branched Mach-Zehnder vs `BeamMix` with tilt and phase; tracked Gaussian vs wave optics | 0.1 % smooth beams; 3 to 10 % hard edges, limited by LightPipes' 0/1 masks at N = 256 | +| cross-code | Raman soliton self-frequency shift, T0 = 50 fs, N = 1, 3 to 6 m (`test_raman_shift.py`, slow): engine vs **pyNLO** rate (1 %), vs the exact-gain rate of a fixed-width soliton `-γ f_R ∫ (h_R * P) P' dt / E` (3 %), Gordon's first-moment rate 20-30 % low; the exact rate reduces to Gordon's for a narrow response (1 %) | 0.3 % ... 3 % | | cross-code | branched arms vs **pyNLO** (`test_pynlo_branched.py`): a fibre after a splitter, before one, and in both arms of a Mach-Zehnder | 0.5 % | | cross-code | DFG 1064 + 1550 → 3.39 µm in MgO:PPLN vs **pyNLO** three-wave mixing (energies, idler spectrum) | 0.1-0.7 % small signal, 0.5 % at gain 50 | diff --git a/install.py b/install.py new file mode 100644 index 0000000..15f3a89 --- /dev/null +++ b/install.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python3 +"""Selective installer / uninstaller for UltrafastEngine releases (standard library only). + + python install.py --list # components, sizes, what is installed + python install.py --include python tests examples.Branched # download and install only these + python install.py --include cli plugins.fitting --dest ~/uf # CLI and one plugin + python install.py --include examples.Branched.pump-probe # one example (shared helpers come with it) + python install.py --include all --exclude media # everything that is built for this platform + python install.py --uninstall --include tests # remove part ... + python install.py --uninstall # ... or all + python install.py --include docs examples --source git # source folders by git partial clone + +A release is a ``manifest.json`` plus one ``.tar.gz`` per component and platform (written by ``build/package.py --components``). +The installer reads the manifest, resolves the names (a name selects its sub-components: ``examples`` = every ``examples.``; +dependencies come along; ``--exclude`` removes names), downloads only those assets, checks their SHA-256, unpacks them under ``--dest`` +and writes ``/.ultrafast-install.json`` listing every file it wrote with its hash. Uninstall removes exactly those files +(a file you changed is left in place with a warning), so a second ``--include`` adds to an install and ``--version`` upgrades it. +Python wheels are installed with ``pip install --find-links /python`` (never from PyPI; their dependencies are). +The ZOS-API (Ansys) is never installed or removed. Guide: docs/releasing.md. + +Main functions: ``fetch_manifest``, ``resolve`` (names -> plan), ``install``, ``uninstall``, ``detect_platform``. +""" +from __future__ import annotations + +import argparse +import hashlib +import io +import json +import os +import platform as _platform +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +from pathlib import Path, PurePosixPath + +RECORD = ".ultrafast-install.json" +REPOSITORY = "https://github.com/JosephCodemard/UltrafastEngine" +ZOSAPI_SUFFIXES = (".dll", ".xml", ".pdb") + + +class InstallError(Exception): + """A problem the user can fix (bad name, hash mismatch, missing asset); printed without a traceback.""" + + +# ---------------------------------------------------------------------------------------------------- platform +def detect_platform() -> str: + """The release platform name of this computer: win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64, osx-x64.""" + system = _platform.system().lower() + machine = _platform.machine().lower() + arch = "arm64" if machine in ("arm64", "aarch64") else "x64" if machine in ("x86_64", "amd64") else "x86" if machine in ("i386", "i686", "x86") else machine + os_name = {"windows": "win", "linux": "linux", "darwin": "osx"}.get(system, system) + return "%s-%s" % (os_name, arch) + + +# ---------------------------------------------------------------------------------------------------- manifest +def base_url_for(version: str | None, repository: str = REPOSITORY) -> str: + """Where manifest.json and the assets live: the latest release, or the release of tag ``v``.""" + if not version or version == "latest": + return repository + "/releases/latest/download" + return "%s/releases/download/v%s" % (repository, version.lstrip("v")) + + +def _open(url: str): + if "://" not in url: + url = Path(url).resolve().as_uri() + return urllib.request.urlopen(url, timeout=60) + + +def fetch_manifest(base: str) -> dict: + """Read ``/manifest.json`` (``base``: https:// or file:// URL, or a folder).""" + url = base.rstrip("/") + "/manifest.json" + try: + with _open(url) as r: + m = json.loads(r.read().decode("utf-8")) + except Exception as e: # noqa: BLE001 -- any failure to fetch is reported the same way + raise InstallError("cannot read %s: %s" % (url, e)) + if m.get("product") != "UltrafastEngine" or "components" not in m: + raise InstallError("%s is not an UltrafastEngine release manifest" % url) + return m + + +# ---------------------------------------------------------------------------------------------------- resolution +def _children(components: dict, name: str) -> list[str]: + return [c for c in components if c == name or c.startswith(name + ".")] + + +def _split_selector(components: dict, sel: str): + """Match a selector to components: ``(names, only)``. ``only`` is a path prefix for a name deeper than any component + (``examples.Branched.pump-probe``): the group is installed, but only that part of it (plus the files directly in the group folder).""" + if sel in ("all", "everything"): + return [c for c, d in components.items() if d.get("inAll", True) and (d.get("assets") or d.get("sourcePaths") or d.get("kind") == "built")], None + names = _children(components, sel) + if names: + return names, None + parts = sel.split(".") + for n in range(len(parts) - 1, 0, -1): + head = ".".join(parts[:n]) + if head in components and components[head].get("subpath"): + rest = "/".join(parts[n:]) + return [head], components[head]["subpath"].replace("{rest}", rest) + known = sorted(components) + raise InstallError("unknown component '%s'. Known: %s" % (sel, ", ".join(known))) + + +def expand(components: dict, include: list[str], exclude: list[str] | None = None): + """Names -> ``(chosen, explicit, required, notes)``: ``chosen`` maps component -> list of only-prefixes (None = whole); ``explicit`` are the + leaves asked for by their own name; ``required`` came in as dependencies. Works on a manifest and on the packager's definitions.""" + notes: list[str] = [] + chosen: dict[str, list[str] | None] = {} + explicit: set[str] = set() + for sel in include: + names, only = _split_selector(components, sel) + for n in names: + if sel == n: + explicit.add(n) + if only is None: + chosen[n] = None + elif chosen.get(n, []) is not None: + chosen.setdefault(n, []) + chosen[n].append(only) # type: ignore[union-attr] + excluded: set[str] = set() + for sel in exclude or []: + for n in _split_selector(components, sel)[0]: + excluded.add(n) + chosen.pop(n, None) + explicit.discard(n) + required: set[str] = set() + queue = list(chosen) + while queue: + n = queue.pop() + for r in components[n].get("requires", []): + if any(r in components[c].get("provides", []) for c in chosen): # python-native provides python + continue + kids = _children(components, r) + if not kids: + raise InstallError("component %s requires %s, which this release does not have" % (n, r)) + for rn in kids: + if rn not in chosen: + chosen[rn] = None + required.add(rn) + queue.append(rn) + if rn in excluded: + notes.append("%s is excluded but %s needs it: included anyway" % (rn, n)) + elif chosen[rn] is not None: # a partial selection needed whole by a dependency + chosen[rn] = None + return chosen, explicit, required, notes + + +def resolve(components: dict, include: list[str], exclude: list[str] | None = None, platform: str = "linux-x64"): + """Resolve names to a plan: ``{component: {"only": [prefixes] or None, "asset": asset-dict}}``, plus notes. + + * a name selects itself and every ``name.``; ``all`` selects everything except components with ``"inAll": false`` (media); + * ``requires`` are added (transitively); ``--exclude`` removes names (and the children), but never a required one; + * the asset for ``platform`` is used, else the platform-independent one (``any``); a leaf asked for by name without an asset raises, + one that only came from ``all`` / a parent raises only if a dependency needs it, else it is reported in the notes. + """ + chosen, explicit, required, notes = expand(components, include, exclude) + plan: dict[str, dict] = {} + for n in sorted(chosen): + d = components[n] + assets = d.get("assets") or [] + if not assets: + continue # a virtual parent + asset = next((a for a in assets if a["platform"] == platform), None) or next((a for a in assets if a["platform"] == "any"), None) + if asset is None: + msg = "%s is not built for %s (available: %s)" % (n, platform, ", ".join(sorted(a["platform"] for a in assets))) + if n in explicit or n in required: + raise InstallError(msg) + notes.append("skipped: " + msg) + continue + plan[n] = {"only": chosen[n], "asset": asset} + for n in plan: + for c in components[n].get("conflicts", []): + if c in plan: + raise InstallError("%s and %s cannot be installed together" % (n, c)) + if not plan: + raise InstallError("nothing to install for platform %s" % platform) + return plan, notes + + +# ---------------------------------------------------------------------------------------------------- record +def read_record(dest: Path) -> dict: + p = dest / RECORD + if p.exists(): + return json.loads(p.read_text(encoding="utf-8")) + return {"schema": 1, "components": {}, "files": {}, "pip": []} + + +def write_record(dest: Path, rec: dict) -> None: + p = dest / RECORD + if not rec["components"]: + if p.exists(): + p.unlink() + return + dest.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(rec, indent=1, sort_keys=True), encoding="utf-8") + + +def _sha256_file(p: Path) -> str: + h = hashlib.sha256() + with open(p, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def _safe_member(name: str) -> PurePosixPath: + p = PurePosixPath(name) + if p.is_absolute() or ".." in p.parts or not p.parts: + raise InstallError("unsafe path in archive: %s" % name) + if p.name.lower().startswith("zosapi") and p.suffix.lower() in ZOSAPI_SUFFIXES: + raise InstallError("archive contains a ZOS-API file (%s): refusing to install it" % name) + return p + + +# ---------------------------------------------------------------------------------------------------- download +def _download(base: str, asset: dict, workdir: Path) -> Path: + url = base.rstrip("/") + "/" + asset["file"] + target = workdir / asset["file"] + h = hashlib.sha256() + try: + with _open(url) as r, open(target, "wb") as f: + for chunk in iter(lambda: r.read(1 << 20), b""): + h.update(chunk) + f.write(chunk) + except Exception as e: # noqa: BLE001 + raise InstallError("cannot download %s: %s" % (url, e)) + if h.hexdigest() != asset["sha256"]: + target.unlink() + raise InstallError("SHA-256 mismatch for %s (expected %s, got %s): nothing was installed from it" % (asset["file"], asset["sha256"], h.hexdigest())) + return target + + +def _wanted(rel: PurePosixPath, only: list[str] | None) -> bool: + if not only: + return True + s = rel.as_posix() + return any(s.startswith(o.rstrip("/") + "/") or s == o.rstrip("/") for o in only) or _is_group_level(rel, only) + + +def _is_group_level(rel: PurePosixPath, only: list[str]) -> bool: + """Files that sit directly in the group folder (docs/examples/Computing/_optics.py) travel with every part of the group.""" + parent = PurePosixPath(only[0].rstrip("/")).parent + return rel.parent == parent + + +# ---------------------------------------------------------------------------------------------------- install +def _remove_files(dest: Path, rec: dict, paths: list[str], component: str, force: bool, log) -> None: + for rel in paths: + entry = rec["files"].get(rel) + if entry is None: + continue + owners = [o for o in entry["components"] if o != component] + if owners: + entry["components"] = owners + continue + p = dest / rel + if p.exists(): + if not force and _sha256_file(p) != entry["sha256"]: + log("WARNING: %s was changed after installation: left in place" % rel) + rec["files"].pop(rel) + continue + p.unlink() + rec["files"].pop(rel) + _prune_empty(dest, p.parent) + + +def _prune_empty(dest: Path, d: Path) -> None: + dest = dest.resolve() + d = d.resolve() + while d != dest and dest in d.parents: + try: + d.rmdir() + except OSError: + return + d = d.parent + + +def _pip(python: str, args: list[str], log, dry: bool) -> None: + cmd = [python, "-m", "pip"] + args + log(" $ " + " ".join(cmd)) + if dry: + return + p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if p.returncode != 0: + raise InstallError("pip failed (exit %d):\n%s" % (p.returncode, p.stdout[-3000:])) + + +def install(base: str, include: list[str], exclude: list[str] | None, dest: Path, platform: str | None = None, force: bool = False, + dry_run: bool = False, use_pip: bool = True, python: str | None = None, log=print, manifest: dict | None = None) -> dict: + """Install the selected components from the release at ``base`` into ``dest``; returns the new record.""" + platform = platform or detect_platform() + manifest = manifest or fetch_manifest(base) + comps = manifest["components"] + plan, notes = resolve(comps, include, exclude, platform) + for n in notes: + log("note: " + n) + log("UltrafastEngine %s -> %s (%s): %s" % (manifest["version"], dest, platform, ", ".join(plan))) + if dry_run: + for n, p in plan.items(): + log(" %-26s %-12s %8.1f MB %s%s" % (n, p["asset"]["platform"], p["asset"]["size"] / 1e6, p["asset"]["file"], " only " + ", ".join(p["only"]) if p["only"] else "")) + pips = [comps[n]["pip"] for n in plan if comps[n].get("pip")] + for pp in pips: + _pip(python or sys.executable, ["install", "--find-links", str(dest / pp["folder"]), pp["spec"]], log, True) + return read_record(dest) + rec = read_record(dest) + dest.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="uf-install-") as tmp: + tmpd = Path(tmp) + archives = {} + for n, p in plan.items(): # download and verify everything first: a bad hash leaves the install untouched + log(" downloading %s (%s, %.1f MB)" % (n, p["asset"]["file"], p["asset"]["size"] / 1e6)) + archives[n] = _download(base, p["asset"], tmpd) + for n, p in plan.items(): + old = rec["components"].get(n) + if old and p["only"] is None: # reinstall / upgrade of the whole component: drop its old, unmodified files first + _remove_files(dest, rec, [f for f, e in list(rec["files"].items()) if n in e["components"]], n, force, log) + written = 0 + with tarfile.open(archives[n], "r:gz") as tf: + for m in tf.getmembers(): + if not m.isfile(): + continue + rel = _safe_member(m.name) + if not _wanted(rel, p["only"]): + continue + key = rel.as_posix() + target = dest / Path(*rel.parts) + data = tf.extractfile(m).read() # type: ignore[union-attr] + sha = hashlib.sha256(data).hexdigest() + entry = rec["files"].get(key) + if target.exists(): + if entry is None and not force: + log("WARNING: %s exists and was not installed by this tool: left in place" % key) + continue + if entry is not None and _sha256_file(target) not in (entry["sha256"], sha) and not force: + log("WARNING: %s was changed after installation: left in place" % key) + continue + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + if m.mode & 0o111: + target.chmod(target.stat().st_mode | 0o111) + if entry is None: + rec["files"][key] = {"sha256": sha, "components": [n]} + else: + entry["sha256"] = sha + if n not in entry["components"]: + entry["components"].append(n) + written += 1 + cur = rec["components"].get(n) + if p["only"] is None or (cur is not None and cur.get("only") is None): + only_rec = None + else: + only_rec = sorted(set((cur or {}).get("only") or []) | set(p["only"])) + d = comps[n] + rec["components"][n] = {"version": manifest["version"], "platform": p["asset"]["platform"], "only": only_rec, + "requires": d.get("requires", []), "subpath": d.get("subpath"), + "pipDistribution": (d.get("pip") or {}).get("distribution"), "sourcePaths": d.get("sourcePaths", [])} + log(" installed %s (%d files)" % (n, written)) + if use_pip: + for n in plan: + pp = comps[n].get("pip") + if pp: + _pip(python or sys.executable, ["install", "--find-links", str(dest / pp["folder"]), pp["spec"]], log, False) + if pp["distribution"] not in rec["pip"]: + rec["pip"].append(pp["distribution"]) + rec["version"] = manifest["version"] + rec["source"] = "release" + write_record(dest, rec) + return rec + + +# ---------------------------------------------------------------------------------------------------- uninstall +def uninstall(dest: Path, include: list[str] | None, exclude: list[str] | None = None, force: bool = False, dry_run: bool = False, + use_pip: bool = True, python: str | None = None, log=print) -> dict: + """Remove the selected components (all when ``include`` is empty) using the install record; files you changed stay.""" + rec = read_record(dest) + if not rec["components"]: + raise InstallError("nothing installed in %s (no %s)" % (dest, RECORD)) + comps = {n: {} for n in rec["components"]} + if include: + chosen: dict[str, list[str] | None] = {} + for sel in include: + names = _children(comps, sel) if sel not in ("all", "everything") else list(comps) + if names: + for n in names: + chosen[n] = None + continue + # a part of a component (examples.Branched.pump-probe): files below its subpath + parts = sel.split(".") + for k in range(len(parts) - 1, 0, -1): + head = ".".join(parts[:k]) + sub = rec["components"].get(head, {}).get("subpath") + if sub: + chosen.setdefault(head, []) + if chosen[head] is not None: + chosen[head].append(sub.replace("{rest}", "/".join(parts[k:]))) # type: ignore[union-attr] + break + else: + raise InstallError("'%s' is not installed in %s (installed: %s)" % (sel, dest, ", ".join(sorted(rec["components"])))) + else: + chosen = {n: None for n in rec["components"]} + for sel in exclude or []: + for n in _children(comps, sel): + chosen.pop(n, None) + if not force: + for n in rec["components"]: + if n in chosen: + continue + for r in rec["components"][n].get("requires", []): + if any(c == r or c.startswith(r + ".") for c in chosen if chosen[c] is None): + raise InstallError("%s needs %s: remove it too, or use --force" % (n, r)) + log("removing from %s: %s" % (dest, ", ".join(chosen))) + if rec.get("source") == "git": + uninstall_git(dest, list(chosen), rec, dry_run, log) + return rec + if dry_run: + for n, only in chosen.items(): + files = [f for f, e in rec["files"].items() if n in e["components"] and (not only or any(f.startswith(o.rstrip("/") + "/") for o in only))] + log(" %-26s %d files" % (n, len(files))) + return rec + for n, only in chosen.items(): + files = [f for f, e in rec["files"].items() if n in e["components"] and (not only or any(f.startswith(o.rstrip("/") + "/") for o in only))] + _remove_files(dest, rec, files, n, force, log) + if only is None: + rec["components"].pop(n) + log(" removed %s (%d files)" % (n, len(files))) + if use_pip: + wanted = {d for d in rec.get("pip", [])} + still = {rec["components"][n].get("pipDistribution") for n in rec["components"]} + for d in sorted(wanted - still): + _pip(python or sys.executable, ["uninstall", "-y", d], log, False) + rec["pip"].remove(d) + write_record(dest, rec) + if not rec["components"]: + _prune_empty(dest, dest) + return rec + + +# ---------------------------------------------------------------------------------------------------- git (source selection) +def _git(dest: Path | None, *args: str) -> None: + cmd = ["git"] + (["-C", str(dest)] if dest else []) + list(args) + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if r.returncode != 0: + raise InstallError("git failed: %s\n%s" % (" ".join(cmd), r.stdout[-2000:])) + + +def install_git(components: dict, include: list[str], exclude: list[str] | None, dest: Path, version: str, repository: str = REPOSITORY, + dry_run: bool = False, log=print) -> None: + """Source folders only, by git partial clone: ``git clone --filter=blob:none --sparse --branch v`` then a cone-mode + ``sparse-checkout`` of the folders of the selected components (the tag pins the version). Cone mode also brings the files that sit + directly in the parent folders, which is how the shared example helpers travel with a single example. Built components (cli, + wheels, cpp ...) are not in git: use the release for them. Figures cannot be separated from their folders here.""" + if shutil.which("git") is None: + raise InstallError("--source git needs git on the PATH") + chosen: dict[str, list[str] | None] = {} + for sel in include: + names, only = _split_selector(components, sel) + for n in names: + if not components[n].get("sourcePaths"): + if sel == n: + raise InstallError("%s is a built component, not a folder of the repository: use --source release" % n) + continue + if only is None: + chosen[n] = None + elif chosen.get(n, []) is not None: + chosen.setdefault(n, []).append(only) # type: ignore[union-attr] + for sel in exclude or []: + for n in _split_selector(components, sel)[0]: + chosen.pop(n, None) + for n in list(chosen): # dependencies that are folders (examples.shared) + for r in components[n].get("requires", []): + for rn in _children(components, r): + if components[rn].get("sourcePaths") and not components[rn].get("gitViaParents") and rn not in chosen: + chosen[rn] = None + if not chosen: + raise InstallError("nothing to check out") + whole = any("." not in n and components[n].get("sourcePaths") == ["."] for n in chosen) + paths = sorted({sp for n, only in chosen.items() for sp in (only if only else components[n]["sourcePaths"])}) + log("git %s of v%s into %s: %s" % ("clone" if whole else "sparse checkout", version.lstrip("v"), dest, ", ".join(paths))) + if dry_run: + return + if (dest / ".git").exists(): + if not whole: + _git(dest, "sparse-checkout", "add", *paths) + else: + dest.parent.mkdir(parents=True, exist_ok=True) + args = ["clone", "--filter=blob:none"] + ([] if whole else ["--sparse"]) + ["--branch", "v" + version.lstrip("v"), repository, str(dest)] + _git(None, *args) + if not whole: + _git(dest, "sparse-checkout", "set", *paths) + rec = read_record(dest) + for n, only in chosen.items(): + d = components[n] + rec["components"][n] = {"version": version.lstrip("v"), "platform": "any", "only": only, "requires": d.get("requires", []), + "subpath": d.get("subpath"), "pipDistribution": None, "sourcePaths": only or d["sourcePaths"]} + rec["source"] = "git" + write_record(dest, rec) + + +def uninstall_git(dest: Path, chosen: list[str], rec: dict, dry_run: bool, log) -> None: + """Remove components of a git checkout by shrinking the sparse set (git deletes the files and keeps ones with local changes).""" + remaining = sorted({sp for n, d in rec["components"].items() if n not in chosen for sp in d["sourcePaths"]}) + log("git sparse-checkout set %s" % (", ".join(remaining) or "(nothing)")) + if dry_run: + return + if remaining: + _git(dest, "sparse-checkout", "set", *remaining) + for n in chosen: + rec["components"].pop(n) + write_record(dest, rec) + return + st = [l for l in subprocess.run(["git", "-C", str(dest), "status", "--porcelain"], stdout=subprocess.PIPE, text=True).stdout.splitlines() + if not l.endswith(RECORD)] + if st: + log("WARNING: the checkout has local changes: left in place (%s)" % dest) + return + def _writable(func, path, _exc): # Windows: git object files are read-only and rmtree cannot delete them + os.chmod(path, stat.S_IWRITE) + func(path) + shutil.rmtree(dest, onerror=_writable) + + +# ---------------------------------------------------------------------------------------------------- list +def list_components(manifest: dict, dest: Path, platform: str, out=print) -> None: + rec = read_record(dest) + out("UltrafastEngine %s: components for %s (installed in %s: %s)" % (manifest["version"], platform, dest, ", ".join(sorted(rec["components"])) or "nothing")) + out("%-30s %-11s %9s %-9s %s" % ("component", "platform", "size", "installed", "requires / description")) + for n, d in sorted(manifest["components"].items()): + assets = d.get("assets") or [] + if not assets: + continue + a = next((a for a in assets if a["platform"] == platform), None) or next((a for a in assets if a["platform"] == "any"), None) + plat = a["platform"] if a else "(none)" + size = "%.1f MB" % (a["size"] / 1e6) if a else "-" + inst = rec["components"].get(n, {}).get("version", "") + extra = ("[needs %s] " % ", ".join(d["requires"]) if d.get("requires") else "") + d.get("description", "") + ("" if d.get("inAll", True) else " (not in 'all')") + out("%-30s %-11s %9s %-9s %s" % (n, plat, size, inst, extra)) + + +# ---------------------------------------------------------------------------------------------------- command line +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="install.py", description="Install or remove parts of an UltrafastEngine release (docs/releasing.md).", + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split("\n\n")[0]) + ap.add_argument("--include", nargs="*", default=None, metavar="NAME", help="components: python tests examples.Branched ... 'all' = everything built for this platform except media") + ap.add_argument("--exclude", nargs="*", default=None, metavar="NAME", help="components to leave out") + ap.add_argument("--list", action="store_true", help="show the components, their sizes and what is installed") + ap.add_argument("--uninstall", action="store_true", help="remove the --include components (all when none are given)") + ap.add_argument("--version", help="release version (default: latest)") + ap.add_argument("--latest", action="store_true", help="the latest release (default)") + ap.add_argument("--platform", default="auto", help="auto, or win-x64 win-arm64 linux-x64 linux-arm64 osx-arm64 osx-x64") + ap.add_argument("--dest", default=None, help="install folder (default: ./UltrafastEngine)") + ap.add_argument("--source", choices=["release", "git"], default="release", help="release assets (default) or a git partial clone of source folders") + ap.add_argument("--base-url", help="folder or URL holding manifest.json and the assets (default: the GitHub release)") + ap.add_argument("--repository", default=REPOSITORY, help="GitHub repository URL for --source git and the default base URL") + ap.add_argument("--python", help="Python used for pip (default: the one running this script)") + ap.add_argument("--no-pip", action="store_true", help="unpack the wheels but do not run pip") + ap.add_argument("--force", action="store_true", help="overwrite / remove files even if they were changed or not installed by this tool") + ap.add_argument("--dry-run", action="store_true", help="show what would happen") + a = ap.parse_args(argv) + dest = Path(a.dest or "UltrafastEngine").expanduser().resolve() + plat = detect_platform() if a.platform == "auto" else a.platform + try: + if a.uninstall: + uninstall(dest, a.include, a.exclude, a.force, a.dry_run, not a.no_pip, a.python) + return 0 + base = a.base_url or base_url_for(None if a.latest else a.version, a.repository) + manifest = fetch_manifest(base) + if a.list: + list_components(manifest, dest, plat) + return 0 + if not a.include: + raise InstallError("say what to install: --include NAME ... (see --list; 'all' for everything)") + if a.source == "git": + install_git(manifest["components"], a.include, a.exclude, dest, manifest["version"], a.repository, a.dry_run) + else: + install(base, a.include, a.exclude, dest, plat, a.force, a.dry_run, not a.no_pip, a.python, manifest=manifest) + except InstallError as e: + print("error: %s" % e, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/llms.txt b/llms.txt index 8bf4b27..6d4fa85 100644 --- a/llms.txt +++ b/llms.txt @@ -34,6 +34,7 @@ Start with docs/agents.md (how to drive the engine) and AGENTS.md (how to change - [Run spec](docs/reference/run-spec.md) - [Models & overrides](docs/reference/models.md) - [Materials](docs/reference/materials.md) +- [Material catalogue: every material number with its source and status](docs/reference/materials-catalogue.md) - [Gases (rotational Raman)](docs/reference/gases.md) - [Surfaces, losses, polarisation](docs/reference/coatings.md) - [Optical components](docs/reference/components.md) @@ -83,14 +84,12 @@ Start with docs/agents.md (how to drive the engine) and AGENTS.md (how to change - [Roadmap](docs/roadmap.md) - [For AI agents](docs/agents.md) - [Claude Code setup](docs/claude-code.md): CLAUDE.md files, commands, subagents, skills, hooks -- [Releasing](docs/releasing.md): build.sh / build.bat release packages +- [Releasing](docs/releasing.md): build.sh / build.bat release packages, release components, `install.py` (selective install / uninstall) ## Design notes - [Development history](docs/design/history.md) -- [Branched systems (design)](docs/design/branching.md) -- [Plan: what is next](docs/design/plan-next.md) -- [Assessment: adaptive optics and digital twin](docs/design/plan-ao-digital-twin-2026-11.md) -- [Roadmap: adaptive optics, shaping, digital twin](docs/design/roadmap-ao-twin-2026-11.md) +- [Next steps](docs/design/next-steps.md) +- [Backlog (parked design)](docs/design/backlog.md) - [Architecture](docs/architecture/index.md) - [Code map](docs/architecture/code-map.md) - [Key processes](docs/architecture/processes.md) diff --git a/mkdocs.yml b/mkdocs.yml index 7c500a6..b59c000 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,6 +39,7 @@ nav: - Run spec: reference/run-spec.md - Models & overrides: reference/models.md - Materials: reference/materials.md + - Material catalogue (sources, status): reference/materials-catalogue.md - Gases (rotational Raman): reference/gases.md - Surfaces, losses, polarisation: reference/coatings.md - Optical components: reference/components.md @@ -88,11 +89,8 @@ nav: - Releasing (build.sh): releasing.md - Design notes: - Development history: design/history.md - - Branched systems (design): design/branching.md - - "Plan: what is next": design/plan-next.md - - "Assessment: adaptive optics and digital twin": design/plan-ao-digital-twin-2026-11.md - - "Roadmap: adaptive optics, shaping, digital twin": design/roadmap-ao-twin-2026-11.md - - "Plan: November 2026 fixes (done)": design/plan-fixes-2026-11.md + - "Next steps": design/next-steps.md + - "Backlog (parked design)": design/backlog.md markdown_extensions: - admonition - tables diff --git a/overrides/main.html b/overrides/main.html index b1d4352..cd754c7 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -5,6 +5,10 @@ {% block extrahead %} {% endblock %} +{% block content %} +

UltrafastEngine
+ {{ super() }} +{% endblock %} {% block footer %} {% include "footer.html" %}

diff --git a/plugins/CLAUDE.md b/plugins/CLAUDE.md index 1d558ea..7f24793 100644 --- a/plugins/CLAUDE.md +++ b/plugins/CLAUDE.md @@ -18,7 +18,7 @@ engine. Guide: `docs/reference/plugins.md`; extension points in `src/UltrafastEn `OpenClDevice.cs`, `CudaDevice.cs`: vendor C APIs through function pointers, loaded at run time), **one** kernel source for both runtimes (`Kernels.cs`), device steppers mirroring the CPU (`EnvelopeStepper.cs`: ERK4(3)-IP / RK4IP + 1-D operator; `SpatialStepper.cs`), self-test / benchmark (`GpuSelfTest.cs`). Compiled into the NativeAOT - library (`UF_WITH_GPU`). A physics change to `EnvelopeProblem` / `SpatialEnvelopeProblem` must be mirrored in the + library (`UF_WITH_GPU`; `ao` likewise with `UF_WITH_AO`, `AoPlugin.RegisterStatic`). A physics change to `EnvelopeProblem` / `SpatialEnvelopeProblem` must be mirrored in the kernels (or make `DeviceSpec()` return null for it) and checked by `GpuTests` (PoCL runs them without a GPU: docs/plugins/gpu.md). Docs: `docs/plugins/gpu.md`. * **hitran** (`hitran/src`): the `index` / `attenuation` model kind `"hitran"` (`GasLines.cs`: `HitranAirIndex` -- Voigt lines, diff --git a/plugins/ao/src/AoPlugin.cs b/plugins/ao/src/AoPlugin.cs index 1f4657d..bea1a9b 100644 --- a/plugins/ao/src/AoPlugin.cs +++ b/plugins/ao/src/AoPlugin.cs @@ -14,6 +14,12 @@ public sealed class AoPlugin : IPlugin public string Version => PluginVersion; public string Description => "Parametric deformable mirror, spatial light modulator, spectral shaper and AOPDF elements."; + ///

Registers the plugin in-process (hosts that cannot load plugin folders, e.g. the NativeAOT library). + public static void RegisterStatic() + { + try { PluginManager.Register(new AoPlugin(), "built-in"); } catch { } + } + public void Register(PluginContext ctx) { ctx.RegisterElement("deformableMirror", DeformableMirror.Compile, new ComponentSpec("element", "deformableMirror", diff --git a/plugins/ao/src/TemporalShapers.cs b/plugins/ao/src/TemporalShapers.cs index 175f88c..cc75ee2 100644 --- a/plugins/ao/src/TemporalShapers.cs +++ b/plugins/ao/src/TemporalShapers.cs @@ -168,6 +168,19 @@ public static void CompileSpectralShaper(CompilerServices s, SystemElement e, Co SpotRadiusM = w0, DxDLambda = dxdl, CentreLambdaM = lamC, CentreOmega = wC, DispersionMPerRadS = dxdw, OffsetM = s.Num(e, "offsetM", 0), PixelValue = px, Mechanism = "spectral shaper" }; t.Text = string.Format(CultureInfo.InvariantCulture, "spectral shaper {0}: {1} pixels, pixel bandwidth {2:G4} rad/s (replica pulses at {3:G4} s), spot radius {4:G3} m", e.Id, n, t.PixelBandwidthRadS, 2 * Math.PI / t.PixelBandwidthRadS, w0); + // planner hook: a pixelated phase / amplitude mask makes replicas at m x 2 pi / pixel bandwidth (Weiner). The spot of 1/e^2 + // radius w0 blurs the pixel edges and suppresses order m by exp(-(pi m w0 / pitch)^2) in amplitude (the pixel-gap sinc is not + // used: an upper bound). A mask without steps or gaps makes none. At most 8 orders. + bool flat = px.All(v => (v - px[0]).Magnitude < 1e-12) && (t.FillFactor >= 1 || Math.Abs(t.GapAmplitude - px[0].Magnitude) < 1e-12 && px[0].Phase == 0); // gaps of a different transmission also make a comb + if (!flat) + { + double rep = 2 * Math.PI / t.PixelBandwidthRadS; + for (int m = 1; m <= 8; m++) + { + double lv = Math.Exp(-2 * Math.Pow(Math.PI * m * w0 / pitch, 2)); + ce.Satellites.Add((m * rep, lv)); ce.SatelliteDelayS = Math.Max(ce.SatelliteDelayS, m * rep); + } + } ce.Post.Add(t); ce.Notes.Add(t.Text); } @@ -200,6 +213,10 @@ public static void CompileAopdf(CompilerServices s, SystemElement e, CompiledEle } t.Text = string.Format(CultureInfo.InvariantCulture, "aopdf {0}: delay {1:G4} s, GDD {2:G4} s^2, TOD {3:G4} s^3, efficiency {4:G3}{5}", e.Id, t.DelayS, t.GddS2, t.TodS3, t.Efficiency, t.WindowS > 0 ? ", acoustic window " + t.WindowS.ToString("G4", CultureInfo.InvariantCulture) + " s" : ""); + // planner hook: the programmed delay and replica move light in time; the programmed GDD stretches it (GDD x bandwidth, in the planner) + double shift = Math.Abs(t.DelayS) + Math.Abs(t.ReplicaAmplitude != 0 ? t.ReplicaDelayS : 0); + if (shift > 0) { ce.Satellites.Add((shift, 1.0)); ce.SatelliteDelayS = Math.Max(ce.SatelliteDelayS, shift); } + ce.ProgrammedGddS2 = t.GddS2; ce.Post.Add(t); ce.Notes.Add(t.Text); } diff --git a/plugins/twin/python/ultrafast_twin/__main__.py b/plugins/twin/python/ultrafast_twin/__main__.py index 3d5b67c..519ff5d 100644 --- a/plugins/twin/python/ultrafast_twin/__main__.py +++ b/plugins/twin/python/ultrafast_twin/__main__.py @@ -104,7 +104,7 @@ def main(argv: Optional[List[str]] = None) -> int: print(f" {'PASS' if c['passed'] else 'FAIL'} {c['id']}: {c['detail']}") return 0 if r["passed"] else 1 elif args.cmd == "serve": - print("ultrafast twin serve: the MCP server is not implemented yet (docs/design/notes/s4-twin.md, section 5)", file=sys.stderr) + print("ultrafast twin serve: the MCP server is not implemented yet (docs/design/next-steps.md, section 4)", file=sys.stderr) return 2 except (ProjectError, KeyError, ValueError) as e: print(f"ultrafast twin: {e}", file=sys.stderr) diff --git a/python/src/ultrafast/algorithms/sensorless.py b/python/src/ultrafast/algorithms/sensorless.py index 5b5cc76..c43071b 100644 --- a/python/src/ultrafast/algorithms/sensorless.py +++ b/python/src/ultrafast/algorithms/sensorless.py @@ -29,19 +29,38 @@ class OptimiserResult: iterates: List[np.ndarray] = field(default_factory=list) -def spgd(metric: Callable[[np.ndarray], float], x0: np.ndarray, *, iterations: int = 200, gain: float = 1.0, amplitude: float = 0.1, seed: int = 0, - maximise: bool = True, keep_iterates: bool = False) -> OptimiserResult: - """x <- x + gain (J(x + d) - J(x - d)) d for a maximised metric J, d_i = +-``amplitude`` (Bernoulli, independent per channel). Two metric evaluations - per iteration (plus one to log the history). ``gain`` is in units of 1 / metric: the expected step along the gradient is 2 gain amplitude^2 grad J.""" +#: Default of :func:`spgd`'s normalised step: the largest step of one channel in one iteration, in units of ``amplitude`` (measured plateau 0.3 to 1.0, see docs/plugins/ao.md). +SPGD_DEFAULT_STEP = 0.4 + + +def spgd(metric: Callable[[np.ndarray], float], x0: np.ndarray, *, iterations: int = 200, gain: Optional[float] = None, amplitude: float = 0.1, seed: int = 0, + maximise: bool = True, keep_iterates: bool = False, step: float = SPGD_DEFAULT_STEP, memory: float = 0.97) -> OptimiserResult: + """Stochastic parallel gradient descent for a maximised metric J: all channels are shaken at once by d_i = +-``amplitude`` (Bernoulli, independent per + channel) and the metric difference dJ = J(x + d) - J(x - d) drives the update. Two metric evaluations per iteration (plus one to log the history). + + * ``gain=None`` (default): **scale-free**. x <- x + ``step`` (dJ / S) d with S the largest |dJ| of the last ~1 / (1 - ``memory``) iterations (a decaying + maximum), so the step of a channel never exceeds ``step`` x ``amplitude`` and shrinks by itself as the metric flattens. Multiplying the metric by any + positive number (or changing its units) gives the same iterates. ``step`` 0.3 to 1.0 reached at least 0.94 of the optimum on every test problem + (16- and 7-channel phase locking, a 6-mode Strehl, the engine's deformable-mirror focus); choose ``amplitude`` about a tenth of the length over + which the metric changes by a factor 2. + * ``gain`` a number: the classical update x <- x + gain dJ d, with ``gain`` in units of 1 / metric (the expected step along the gradient is + 2 gain amplitude^2 grad J). It works only for one range of metric scales, which you must find by trial (a metric of 1e-3 needs a gain 1e6 times larger). + """ rng = np.random.default_rng(seed) x = np.array(x0, float) sign = 1.0 if maximise else -1.0 res = OptimiserResult(x.copy()) + scale = 0.0 for _ in range(iterations): d = amplitude * rng.choice([-1.0, 1.0], size=x.shape) jp, jm = metric(x + d), metric(x - d) res.evaluations += 2 - x = x + sign * gain * (jp - jm) * d + dj = jp - jm + if gain is None: + scale = max(memory * scale, abs(dj)) + x = x + sign * step * (dj / scale if scale > 0 else 0.0) * d + else: + x = x + sign * gain * dj * d res.history.append(float(metric(x))); res.evaluations += 1 if keep_iterates: res.iterates.append(x.copy()) diff --git a/python/src/ultrafast/types.py b/python/src/ultrafast/types.py index 05c1234..fdeb9cd 100644 --- a/python/src/ultrafast/types.py +++ b/python/src/ultrafast/types.py @@ -48,7 +48,7 @@ class AmplifierElement(TypedDict, total=False): transverse: Union["Transverse", str] """Spatial gain""" thermalLens: Any - """true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens)""" + """true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens)""" slices: int """Slices along z Default: 24.""" segments: int @@ -67,6 +67,8 @@ class AmplifierElement(TypedDict, total=False): """Seed amplified spontaneous emission (1-D) Default: false.""" aseSeed: int """ASE random seed""" + aseModel: Any + """true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase) Default: false.""" temperatureK: float """Crystal temperature [K]""" dopingDensityM3: float @@ -971,7 +973,7 @@ class GainElement(TypedDict, total=False): transverse: Union["Transverse", str] """Spatial gain""" thermalLens: Any - """true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens)""" + """true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens)""" slices: int """Slices along z Default: 24.""" segments: int @@ -990,6 +992,8 @@ class GainElement(TypedDict, total=False): """Seed amplified spontaneous emission (1-D) Default: false.""" aseSeed: int """ASE random seed""" + aseModel: Any + """true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase) Default: false.""" temperatureK: float """Crystal temperature [K]""" dopingDensityM3: float @@ -4226,7 +4230,7 @@ class Grid(TypedDict, total=False): maxN: int """Largest allowed n Default: 4194304.""" snapCarrier: bool - """Put the carrier on the frequency grid Default: true.""" + """Put the carrier on the frequency grid. A cw source is not snapped (its line sits on centerWavelengthM) unless this is written Default: true.""" spectralMargin: float """Spectral span factor Default: auto.""" minSpanFraction: float @@ -4558,6 +4562,10 @@ class Physics(TypedDict, total=False): """Highest harmonic the carrier-resolved grid holds Default: 3.""" absoluteIndex: bool """Add air dispersion to air paths quoted relative to air Default: true.""" + strictMaterialsAllow: List[Any] + """Catalogue quantities that strictMaterials accepts although they are not verified, by catalogue path (e.g. "CS2.n2") as listed in the error and in summary.json unverifiedInputs; matching is case-insensitive and an entry name such as "CS2" accepts all its quantities""" + strictMaterials: bool + """Refuse a run that uses approximate or unverified material-catalogue values (the compile error lists them; override each, or give a verified value in your own catalogue file). The environment variable ULTRAFAST_STRICT_MATERIALS=1 does the same Default: false.""" polarization: Literal["jones", "scalar"] """Jones (two polarisations) or scalar Default: jones.""" losses: bool diff --git a/python/tests/test_ao_algorithms.py b/python/tests/test_ao_algorithms.py index 12b2b55..3bcb03b 100644 --- a/python/tests/test_ao_algorithms.py +++ b/python/tests/test_ao_algorithms.py @@ -235,6 +235,39 @@ def test_spgd_phase_locks_an_array(): assert r.history[-1] > 0.99, seed +def _spgd_problems(): + """(name, metric factory(scale, seed), n, iterations, amplitude): phase locking of 16 and 7 channels (random piston) and a 6-mode Strehl-like peak exp(-2 |x - x*|^2).""" + def lock(N, iters): + def make(scale, seed): + phi = np.random.default_rng(100 + seed).uniform(0, 2 * np.pi, N) + return lambda u: scale * abs(np.exp(1j * (phi + u)).sum()) ** 2 / N ** 2 + return make, N, iters, 0.2 + xs = np.array([-0.25, 0.2, -0.15, 0.2, -0.1, 0.1]) + strehl = (lambda scale, seed: (lambda a: scale * np.exp(-2.0 * np.sum((a - xs) ** 2)))), 6, 120, 0.05 + return {"lock16": lock(16, 300), "lock7": lock(7, 300), "strehl6": strehl} + + +def test_spgd_default_is_scale_free(): + """The default step is normalised by the metric's own recent range, so J, 1e-3 J and 1e3 J give the same iterates (the classical fixed gain is in units of 1 / metric).""" + make, n, iters, amp = _spgd_problems()["lock16"] + ref = sensorless.spgd(make(1.0, 0), np.zeros(n), iterations=60, amplitude=amp, seed=1, keep_iterates=True) + for scale in (1e-3, 1e3): + r = sensorless.spgd(make(scale, 0), np.zeros(n), iterations=60, amplitude=amp, seed=1, keep_iterates=True) + assert np.allclose(np.array(r.iterates), np.array(ref.iterates), rtol=1e-9, atol=1e-12), scale + # the fixed gain is not: the same gain that locks J = 1 does nothing for 1e-3 J + fixed = lambda sc: sensorless.spgd(make(sc, 0), np.zeros(n), iterations=300, gain=10.0, amplitude=amp, seed=1).history[-1] / sc + assert fixed(1.0) > 0.99 and fixed(1e-3) < 0.1 + + +def test_spgd_default_reaches_0p9_of_the_optimum_at_any_metric_scale(): + """Default SPGD (step 0.4 amplitude) on phase locking of 16 and 7 channels and a 6-mode Strehl peak, metric scales 1e-3, 1 and 1e3, 10 seeds each: the lowest final + value over the optimum is at least 0.9 (measured minimum 0.98; the fixed gains that lock at scale 1 give 0.00 to 0.04 at 1e-3 and 1e3).""" + for name, (make, n, iters, amp) in _spgd_problems().items(): + for scale in (1e-3, 1.0, 1e3): + worst = min(sensorless.spgd(make(scale, seed), np.zeros(n), iterations=iters, amplitude=amp, seed=seed).history[-1] / scale for seed in range(10)) + assert worst > 0.9, (name, scale, worst) + + def test_modal_search_is_exact_for_a_quadratic_separable_metric_in_one_sweep(): """Parabola vertex from three points: exact for J = -sum_i a_i (x_i - x_i*)^2 in one sweep (2 N + 1 evaluations).""" a = np.array([1.0, 2.0, 0.5, 3.0]); xs = np.array([0.4, -0.7, 0.9, 0.1]) diff --git a/python/tests/test_ao_plugin.py b/python/tests/test_ao_plugin.py index 9be098d..4b39cfa 100644 --- a/python/tests/test_ao_plugin.py +++ b/python/tests/test_ao_plugin.py @@ -14,7 +14,7 @@ import ultrafast as uf # noqa: E402 from ultrafast import build as b # noqa: E402 -from ultrafast.algorithms import sensorless, zernike # noqa: E402 +from ultrafast.algorithms import holography, sensorless, zernike # noqa: E402 from ultrafast_ao import DmBench, devices, tiled_array, combining_metrics # noqa: E402 @@ -240,6 +240,20 @@ def metric(u): N = n_saved +def test_spgd_default_focuses_the_deformable_mirror_bench_from_the_camera_peak(): + """SPGD with the default (scale-free) step on the engine's deformable-mirror bench: six hidden aberration modes (waves rms), the camera peak fluence over its reference + is the only measurement, amplitude 0.05, 80 iterations (240 engine runs): the Strehl ratio rises from about 0.4 to at least 0.9 on 3 seeds (the fixed gain needed 3 by + trial: 1.5 reached 0.64, 5 reached 0.80).""" + aberration = {4: -0.25, 5: 0.20, 6: -0.15, 7: 0.20, 8: -0.10, 9: 0.10} + modes = sorted(aberration) + bench = DmBench(aberration=aberration) + ref = bench.reference_peak() + metric = lambda a: bench.peak_fluence(bench.zernike_commands(dict(zip(modes, a)))) / ref + start = metric(np.zeros(6)) + finals = [sensorless.spgd(metric, np.zeros(6), iterations=80, amplitude=0.05, seed=s).history[-1] for s in (3, 4, 5)] + assert start < 0.5 and min(finals) > 0.9, (start, finals) + + # ----------------------------------------------------------------------------------------------- closed-loop pulse shaping def _peak_power(gdd_aopdf_fs2, chirp_fs2): sysd = b.system("s", [b.gap("g", 0.0, medium="Vacuum"), {"id": "chirp", "type": "aopdf", "gddFs2": chirp_fs2}, {"id": "shape", "type": "aopdf", "gddFs2": float(gdd_aopdf_fs2)}]) @@ -255,3 +269,153 @@ def test_closed_loop_shaping_finds_the_compensating_gdd_from_the_peak_power(): metric = lambda g: _peak_power(g[0], chirp) r = sensorless.modal_search(metric, np.array([0.0]), bias=300.0, sweeps=6) assert abs(r.x[0] + chirp) < 2.0, r.x + + +# ----------------------------------------------------------------------------------------------- hologram encodings through the SLM element +def _slm_focal_fluence(phase_map, illum_radius, n=64, dx=40e-6, lam=800e-9, f=0.2): + """Phase map (rad) -> 256 linear grey levels -> engine SLM element -> lens -> focal plane; returns (fluence map, focal axis, array-plane axis, quantised phase).""" + from ultrafast_ao import Slm + lut = devices.slm_lut(256) + grey = holography.grey_levels(phase_map, lut) + g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * dx / 2, spectral_samples=1, absorbing_boundary=True, initial_profile=ip) + sysd = b.system("h", [b.gap("src", 0.0, medium="Vacuum", transverse=g(b.initial_profile("gaussian", radius_m=illum_radius))), + Slm("slm", dx, pixels_x=n, pixels_y=n, grey_level=grey.astype(float), dispersion="none"), b.lens("lens", f), b.gap("z", f, medium="Vacuum", transverse=g())]) + d = uf.run(sysd, b.run_spec(b.cw(lam, 1.0), instruments=[b.instrument("beamProfile", at="z", name="cam")])).instrument("cam") + X = (np.arange(n) - (n - 1) / 2) * dx + return np.asarray(d["fluence_map_j_m2"]), np.asarray(d["x_m"]), X, lut[grey] + + +def _fourier_power(U, X, xf, lam=800e-9, f=0.2, shift_x=0.0): + """|sum U(X) exp(-2 pi i xf X / (lam f))|^2 on the focal grid (x = first array axis), the focal coordinate x shifted by ``shift_x``.""" + Kx = np.exp(-2j * np.pi * np.outer(xf + shift_x, X) / (lam * f)) + Ky = np.exp(-2j * np.pi * np.outer(xf, X) / (lam * f)) + return np.abs(Kx @ U @ Ky.T) ** 2 + + +def _smooth_complex_target(X, w): + """A slowly varying complex field: a ramp in amplitude (0.35 .. 1) and a gentle quadratic + tilt phase (1.5 rad over the beam radius).""" + Xa, Ya = np.meshgrid(X, X, indexing="ij") + amp = 0.35 + 0.65 * (0.5 + 0.5 * np.tanh(Ya / (0.5 * w))) + phi = 1.5 * (Xa ** 2 + Ya ** 2) / w ** 2 + 0.5 * Xa / w + return amp * np.exp(1j * phi) + + +def test_bolduc_first_order_through_the_slm_carries_a_varying_complex_field(): + """Bolduc et al., Opt. Lett. 38, 3546 (2013), end to end: the phase map of a slowly varying A(x, y) e^{i phi(x, y)} (amplitude 0.35 .. 1, 1.5 rad of quadratic phase over the beam radius: the encoding needs the field to change little over one 8-pixel period; with 4 rad the first order of the *ideal* hologram already correlates only 0.86 with the transform of the target), blazed with a period of + 8 pixels, goes through 256 grey levels, the engine's SLM element, a lens and a gap. In the focal plane the first order, at x = lambda f / (8 pitch) from the axis, is + the Fourier transform of illumination x A e^{i phi} (the same numpy sum as the engine's field, evaluated at the shifted coordinate): correlation of the fluence maps in + that order, and the first-order energy over that of a unit-amplitude blaze, against the same ratio of the transforms.""" + n, dx, w, lam, f, L = 64, 40e-6, 0.6e-3, 800e-9, 0.2, 8 + target = _smooth_complex_target((np.arange(n) - (n - 1) / 2) * dx, w) + psi = holography.bolduc_encode(target, period_pixels=L, axis=0) + F, xf, X, _ = _slm_focal_fluence(psi, w, n, dx, lam, f) + illum = np.exp(-(X[:, None] ** 2 + X[None, :] ** 2) / w ** 2) + x1 = lam * f / (L * dx) + P = _fourier_power(illum * target, X, xf, lam, f, shift_x=-x1) # first order: the carrier exp(+2 pi i x / (L dx)) moves the transform by x1 + XF, YF = np.meshgrid(xf, xf, indexing="ij") + win = (np.abs(XF - x1) < 0.25 * x1) & (np.abs(YF) < 0.5 * x1) + corr = np.corrcoef(P[win], F[win])[0, 1] + assert corr > 0.99, corr + # the energy carried by the amplitude: first-order energy over that of an engine run with A = 1 (a plain blaze) against the same ratio of the transforms. The ratio cancels + # the engine's own first-order efficiency of a sampled blaze (0.85 at 8 pixels per period against 1.00 for the Fourier sum: the open SLM finding of docs/design/next-steps.md, WP7) + ref = _slm_focal_fluence(holography.bolduc_encode(np.ones((n, n)), period_pixels=L, axis=0), w, n, dx, lam, f)[0] + order = np.abs(XF - x1) < 0.4 * x1 + got = F[order].sum() / ref[order].sum() + want = _fourier_power(illum * target, X, xf, lam, f, shift_x=-x1)[order].sum() / _fourier_power(illum * np.exp(2j * np.pi * np.arange(n)[:, None] / L), X, xf, lam, f)[order].sum() + assert abs(got / want - 1) < 0.03, (got, want) # measured 1.1 % + + +def test_double_phase_through_the_slm_carries_a_varying_complex_field_at_low_frequency(): + """Double phase (A e^{i phi} = (e^{i(phi + a)} + e^{i(phi - a)})/2, a = arccos A, on a checkerboard), end to end through the SLM element: the checkerboard's frequency + is 1/(2 pitch), a focal distance lambda f / (2 pitch) = 2 mm from the axis, outside the 1.28 mm half width of the focal grid, so the field near the axis is the Fourier + transform of illumination x A e^{i phi}: correlation of the fluence maps within 0.8 mm of the axis, and the energy there over the incident energy (a flat-phase run) against the same ratio of the transform of the target.""" + n, dx, w, lam, f = 64, 40e-6, 0.6e-3, 800e-9, 0.2 + target = _smooth_complex_target((np.arange(n) - (n - 1) / 2) * dx, w) + F, xf, X, _ = _slm_focal_fluence(holography.double_phase_encode(target), w, n, dx, lam, f) + illum = np.exp(-(X[:, None] ** 2 + X[None, :] ** 2) / w ** 2) + P = _fourier_power(illum * target, X, xf, lam, f) + XF, YF = np.meshgrid(xf, xf, indexing="ij") + win = np.hypot(XF, YF) < 0.8e-3 + corr = np.corrcoef(P[win], F[win])[0, 1] + assert corr > 0.99, corr + F0 = _slm_focal_fluence(np.zeros((n, n)), w, n, dx, lam, f)[0] # flat phase: the incident energy (the checkerboard light leaves the focal grid) + expect = P[win].sum() / _fourier_power(illum, X, xf, lam, f).sum() + assert abs(F[win].sum() / F0.sum() / expect - 1) < 0.03, (F[win].sum() / F0.sum(), expect) # measured 0.5 %; correlation 0.99998 + + +# ----------------------------------------------------------------------------------------------- tiled array: lobe fractions +def test_tiled_array_lobe_fractions_match_the_array_factor_for_in_phase_and_checkerboard_pistons(): + """combining_metrics' central and grating lobe fractions are numbers, not only inside (0, 1): a 3 x 3 square array of Gaussian tiles (radius w, pitch p) has the far field + exp(-(pi w r / (lambda F))^2) x sum_k exp(i phi_k) exp(-2 pi i p_k . r / (lambda F)); the fractions of that analytic fluence on the engine's grid, with the same lobe radii, + equal the engine's to 1e-3 (measured 2e-5). In phase, and with pistons pi on the checkerboard (the peak moves to lambda F / (2 p) and the central lobe fraction is measured around it); + central + grating <= 1 in both.""" + p, w = 3e-3, 0.5e-3 + pos = [((i - 1) * p, (j - 1) * p) for i in range(3) for j in range(3)] + par = np.array([(i + j) % 2 for i in range(3) for j in range(3)]) + for name, piston in (("in phase", np.zeros(9)), ("checkerboard pi", np.pi * par)): + prof = tiled_array(w, layout="square", count_x=3, count_y=3, pitch_m=p, piston_rad=piston.tolist()) + F, x, y = _tile_axis_fluence(prof, b.cw(LAM, 1.0)) + got = combining_metrics(F, x, y, wavelength_m=LAM, focal_length_m=0.5, pitch_m=p, n_tiles=9) + X, Y = np.meshgrid(x, y, indexing="ij") + field = sum(np.exp(1j * ph) * np.exp(-2j * np.pi * (px * X + py * Y) / (LAM * 0.5)) for (px, py), ph in zip(pos, piston)) + ref = np.abs(field) ** 2 * np.exp(-2 * (np.pi * w) ** 2 * (X ** 2 + Y ** 2) / (LAM * 0.5) ** 2) + want = combining_metrics(ref, x, y, wavelength_m=LAM, focal_length_m=0.5, pitch_m=p, n_tiles=9) + for key in ("central_lobe_fraction", "grating_lobe_fraction"): + assert abs(got[key] - want[key]) < 1e-3, (name, key, got[key], want[key]) + assert got["central_lobe_fraction"] + got["grating_lobe_fraction"] <= 1 + 1e-9 + + +# ----------------------------------------------------------------------------------------------- SLM pixels on the engine grid (WP7) +def _blaze_total(period, offset_m, n=64, dx=40e-6, lam=800e-9, f=0.2, w=0.5e-3): + """Total focal-plane fluence of an ideal sawtooth of ``period`` pixels, over that of a flat phase map, and the run's warnings.""" + from ultrafast_ao import Slm + lut = devices.slm_lut(256) + g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=n * dx / 2, spectral_samples=1, absorbing_boundary=False, initial_profile=ip) + + def run(phase): + grey = holography.grey_levels(phase, lut).astype(float) + sysd = b.system("h", [b.gap("src", 0.0, medium="Vacuum", transverse=g(b.initial_profile("gaussian", radius_m=w))), + Slm("slm", dx, pixels_x=n, pixels_y=n, grey_level=grey, dispersion="none", offset_x_m=offset_m, offset_y_m=offset_m), + b.lens("lens", f), b.gap("z", f, medium="Vacuum", transverse=g())]) + d = uf.run(sysd, b.run_spec(b.cw(lam, 1.0), instruments=[b.instrument("beamProfile", at="z", name="cam")])) + return float(np.asarray(d.instrument("cam")["fluence_map_j_m2"]).sum()), d.summary.get("warnings", []) + blaze = 2 * np.pi * (np.arange(n)[:, None] % period) / period * np.ones((1, n)) + (e, warn), (e0, _) = run(blaze), run(np.zeros((n, n))) + return e / e0, warn + + +@pytest.mark.parametrize("period", [8, 16]) +def test_slm_pixels_on_the_grid_points_keep_all_the_light_of_a_blaze_and_half_a_pixel_off_keeps_cos_squared(period): + """The engine grid has a sample on the axis; an even panel has a pixel edge there, so with pitch = grid step every grid cell averages two pixels and a blaze of P pixels + per period keeps cos^2(pi / P) of its light (0.854 at 8, 0.962 at 16: the SLM-against-Fourier finding of WP7). Shifted by half a pixel the cell centres sit on the grid + points and no light is lost (1e-6); the unaligned case warns.""" + dx = 40e-6 + straddled, warn = _blaze_total(period, 0.0) + aligned, warn_aligned = _blaze_total(period, -dx / 2) + assert straddled == pytest.approx(np.cos(np.pi / period) ** 2, abs=2e-3) + assert aligned == pytest.approx(1.0, abs=1e-6) + assert any("averages two device cells" in w for w in warn) + assert not any("averages two device cells" in w for w in warn_aligned) + + + +def _tile_axis_fluences(n, f): + """On-axis fluence of each of the 7 hex tiles alone (the engine normalises the power, so identical tiles must give identical values), on n cells over +-5 mm, lens f.""" + out = [] + for k in range(7): + g = lambda ip=None: b.transverse("cartesian", nx=n, ny=n, half_width_m=5e-3, spectral_samples=1, absorbing_boundary=True, initial_profile=ip) + prof = tiled_array(0.5e-3, layout="hex", rings=1, pitch_m=3e-3, amplitudes=[1.0 if i == k else 0.0 for i in range(7)]) + sysd = b.system("t", [b.gap("src", 0.0, medium="Vacuum", transverse=g(prof)), b.lens("lens", f), b.gap("z", f, medium="Vacuum", transverse=g())]) + d = uf.run(sysd, b.run_spec(b.cw(800e-9, 1.0), instruments=[b.instrument("beamProfile", at="z", name="cam")])).instrument("cam") + out.append(float(np.asarray(d["fluence_map_j_m2"])[n // 2, n // 2])) + return np.array(out) + + +def test_tiled_array_identical_tiles_reach_the_axis_alike_only_when_the_lens_is_resolved(): + """WP7 (SPGD ended at 1.02 of the in-phase value): the 7 tiles are identical, so their axis fluences must be equal. On 128 cells over 10 mm at f = 0.5 m the lens phase at a + tile 3 mm out changes by 3.7 rad per cell (more than pi): the lens is aliased and the axis fluences differ by a factor of 56, so the zero-piston sum is not the maximum. With + f = 1 m on 128 cells (1.9 rad per cell) or 256 cells at f = 0.5 m they agree to 1e-3.""" + assert _tile_axis_fluences(128, 0.5).max() / _tile_axis_fluences(128, 0.5).min() > 10 + for n, f in ((128, 1.0), (256, 0.5)): + v = _tile_axis_fluences(n, f) + assert v.max() / v.min() < 1.001, (n, f, v) diff --git a/python/tests/test_packaging.py b/python/tests/test_packaging.py index a3c1593..c32da05 100644 --- a/python/tests/test_packaging.py +++ b/python/tests/test_packaging.py @@ -82,3 +82,27 @@ def test_dry_run_prints_the_plan(capsys): assert pkg.main(["--all", "--plugins", "fitting", "--platform", "host", "--dry-run"]) == 0 out = capsys.readouterr().out assert "dotnet publish" in out and "UltrafastEngine.Cli.csproj" in out + + +def test_ao_plugin_is_compiled_into_the_native_library_only_when_selected(): + with_ao = pkg.parse(["--cpp", "--plugins", "ao"]) + without = pkg.parse(["--cpp", "--plugins", "fitting"]) + B = lambda a: type("B", (), {"a": a})() + assert pkg.Builder._ao_flag(B(with_ao)) == [] + assert pkg.Builder._ao_flag(B(without)) == ["-p:UF_WITH_AO=false"] + assert pkg.parse(["--plugins", "slm"]).plugins == ["ao"] + + +def test_native_library_lists_the_four_ao_elements(): + """Needs a built NativeAOT library: set ULTRAFAST_NATIVE_LIB to libultrafast.so / .dylib / ultrafast.dll (skipped otherwise).""" + import ctypes, json, os + path = os.environ.get("ULTRAFAST_NATIVE_LIB") + if not path or not os.path.exists(path): + pytest.skip("no NativeAOT library (set ULTRAFAST_NATIVE_LIB)") + lib = ctypes.CDLL(path) + out = ctypes.c_char_p() + assert lib.uf_capabilities(ctypes.byref(out)) == 0 + text = out.value.decode() + lib.uf_free(out) + for name in ("deformableMirror", "slm", "spectralShaper", "aopdf"): + assert name in text, name + " is missing from the native library's capabilities" diff --git a/python/tests/test_raman_shift.py b/python/tests/test_raman_shift.py new file mode 100644 index 0000000..75562c4 --- /dev/null +++ b/python/tests/test_raman_shift.py @@ -0,0 +1,75 @@ +"""Raman soliton self-frequency shift (WP7): the engine against pyNLO and against the exact-gain rate of a fixed-width soliton. + +Gordon's -8 T_R |beta2| / (15 T0^4) uses the first moment of the Raman response and is 15-40 % low for 40-65 fs solitons; the rate computed with the +full Blow-Wood response (same physics, no first-moment approximation) is what the engine must reproduce. See docs/examples/Fibres/raman-soliton-shift. +""" +import numpy as np +import pytest + +from conftest import import_pynlo, requires_engine, slow + +import ultrafast as uf +from ultrafast import build as b + +LAM, B2, GAMMA = 1550e-9, -20e-27, 1.3e-3 +TAU1, TAU2, FR = 12.2e-15, 32e-15, 0.18 +T0 = 50e-15 + + +def _exact_rate(t0): + """-gamma f_R / E * integral (h_R * P) P' dt, rad/s per m (see raman_shift.exact_rate).""" + dt = 0.25e-15 + t = np.arange(-4e-12, 4e-12, dt) + ts = np.arange(0, 2e-12, dt) + h = (TAU1 ** 2 + TAU2 ** 2) / (TAU1 * TAU2 ** 2) * np.exp(-ts / TAU2) * np.sin(ts / TAU1) + p = abs(B2) / (GAMMA * t0 ** 2) / np.cosh(t / t0) ** 2 + f = np.convolve(p, h * dt)[:len(p)] + return -GAMMA * FR * abs(np.trapezoid(f * np.gradient(p, dt), t)) / np.trapezoid(p, t) + + +def test_exact_rate_reduces_to_gordon_for_a_narrow_response(): + """The same response with tau1 and tau2 scaled by 1/20 (T_R / 20, a memory far shorter than the soliton): the exact rate equals Gordon's formula to 1 %.""" + dt = 0.1e-15 + t = np.arange(-1.5e-12, 1.5e-12, dt) + ts = np.arange(0, 0.2e-12, dt) + t1, t2 = TAU1 / 20, TAU2 / 20 + h = (t1 ** 2 + t2 ** 2) / (t1 * t2 ** 2) * np.exp(-ts / t2) * np.sin(ts / t1) + tr = FR * np.trapezoid(ts * h, ts) / np.trapezoid(h, ts) + p = abs(B2) / (GAMMA * T0 ** 2) / np.cosh(t / T0) ** 2 + f = np.convolve(p, h * dt / np.trapezoid(h, ts))[:len(p)] + rate = GAMMA * FR * abs(np.trapezoid(f * np.gradient(p, dt), t)) / np.trapezoid(p, t) + assert rate == pytest.approx(8 * tr * abs(B2) / (15 * T0 ** 4), rel=0.01) + + +def _engine_centroid(length): + fib = {"id": "fibre", "type": "fiber", "lengthM": length, "betas": [B2], "gamma": GAMMA, "raman": "silica", "referenceWavelengthM": LAM} + pulse = b.pulse(LAM, 1.763 * T0, peak_power_w=abs(B2) / (GAMMA * T0 ** 2), shape="sech2") + r = uf.run(b.system("raman", [fib]), b.run_spec(pulse, physics={"selfSteepening": False, "raman": True}, grid={"auto": False, "n": 8192, "windowS": 20e-12})) + w, s = r["output_omega_rad_s"], r["output_spectrum"] + return float(np.sum(w * s) / np.sum(s)) + + +def _pynlo_centroid(pynlo, length): + p0 = abs(B2) / (GAMMA * T0 ** 2) + pulse = pynlo.light.DerivedPulses.SechPulse(power=1, T0_ps=T0 * 1e12, center_wavelength_nm=LAM * 1e9, time_window_ps=20, GDD=0, TOD=0, NPTS=2 ** 14, frep_MHz=100, power_is_avg=False) + pulse.set_epp(2 * p0 * T0) + fiber = pynlo.media.fibers.fiber.FiberInstance() + fiber.generate_fiber(length, center_wl_nm=LAM * 1e9, betas=(B2 * 1e27,), gamma_W_m=GAMMA, gvd_units="ps^n/km", gain=0) + evol = pynlo.interactions.FourWaveMixing.SSFM.SSFM(local_error=0.0005, USE_SIMPLE_RAMAN=True, disable_Raman=False, disable_self_steepening=True, suppress_iteration=True) + _, _, _, pout = evol.propagate(pulse_in=pulse, fiber=fiber, n_steps=60) + f, s = pout.F_THz * 1e12, np.abs(pout.AW) ** 2 + return 2 * np.pi * float(np.sum(f * s) / np.sum(s)) + + +@requires_engine +@slow +def test_raman_soliton_shift_matches_pynlo_and_the_exact_gain_rate(): + """T0 = 50 fs, N = 1, 3 to 6 m: engine rate = pyNLO rate (1 %) = exact-gain rate of a fixed-width soliton (3 %), and Gordon's first-moment rate is 20-30 % low.""" + pynlo = import_pynlo() + eng = (_engine_centroid(6.0) - _engine_centroid(3.0)) / 3.0 + ref = (_pynlo_centroid(pynlo, 6.0) - _pynlo_centroid(pynlo, 3.0)) / 3.0 + exact = _exact_rate(T0) + assert eng == pytest.approx(ref, rel=0.01) + assert eng == pytest.approx(exact, rel=0.03) + tr = 1.462e-15 + assert eng / (-8 * tr * abs(B2) / (15 * T0 ** 4)) > 1.2 diff --git a/python/tests/test_release.py b/python/tests/test_release.py new file mode 100644 index 0000000..000459a --- /dev/null +++ b/python/tests/test_release.py @@ -0,0 +1,408 @@ +"""Release system (WP8): component resolution, install.py against a local fake release, the ZOS-API gate, the git sparse path. + +Everything runs on small synthetic releases in a temporary folder (no network, no .NET): the assets are written by the real +``build/release_components.py`` writer, the manifest by its ``manifest()``, and the installer is the real ``install.py``. +""" +import importlib.util +import io +import json +import os +import shutil +import subprocess +import sys +import tarfile +import zipfile +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parents[2] + + +def _load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +inst = _load("install", REPO / "install.py") +rc = _load("release_components", REPO / "build" / "release_components.py") +pkg = _load("package_for_release_tests", REPO / "build" / "package.py") + + +# ---------------------------------------------------------------------------------------------------- a fake release +def _defs(): + d = { + "cli": dict(kind="built", perPlatform=True, description="cli"), + "python": dict(kind="built", perPlatform=False, description="py", pip=dict(folder="python", distribution="ultrafast-engine", spec="ultrafast-engine[hosted]")), + "python-native": dict(kind="built", perPlatform=True, description="native", conflicts=["python"], provides=["python"], inAll=False), + "plugins.fitting": dict(kind="built", description="fit", requires=["cli"]), + "plugins.twin": dict(kind="built", description="twin", requires=["cli", "plugins.fitting"]), + "examples.shared": dict(kind="source", description="shared", sourcePaths=["docs/examples"], gitViaParents=True), + "examples.Branched": dict(kind="source", description="b", requires=["examples.shared", "python"], sourcePaths=["docs/examples/Branched"], + subpath="docs/examples/Branched/{rest}"), + "examples.Fibres": dict(kind="source", description="f", requires=["examples.shared", "python"], sourcePaths=["docs/examples/Fibres"], + subpath="docs/examples/Fibres/{rest}"), + "media": dict(kind="source", description="m", inAll=False, sourcePaths=["docs/examples"]), + "tests": dict(kind="source", description="t", requires=["python"], sourcePaths=["tests"]), + } + for v in d.values(): + v.setdefault("requires", []) + v.setdefault("default", False) + return d + + +FILES = { + "docs/examples/_figures.py": "shared", + "docs/examples/Branched/pump-probe/run.json": "{}", + "docs/examples/Branched/pump-probe/pp.py": "print(1)", + "docs/examples/Branched/cavity/run.json": "{}", + "docs/examples/Branched/make_all.py": "group helper", + "docs/examples/Fibres/soliton/run.json": "{}", + "tests/a.py": "a", +} + + +def make_release(tmp: Path, version="1.0.0", content_tag="") -> Path: + out = tmp / ("release-" + version) + out.mkdir(parents=True, exist_ok=True) + src = tmp / ("src-" + version) + for rel, text in FILES.items(): + p = src / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text + content_tag) + defs = _defs() + assets = {} + + def put(name, plat, files): + info = rc.write_asset(files, out / rc.asset_name(name, version, plat)) + assets.setdefault(name, []).append(dict(platform=plat, **info)) + + for n, d in defs.items(): + if d["kind"] == "source" and n != "media": + roots = d["sourcePaths"] + fl = [(r, src / r) for r in FILES if any(r.startswith(x + "/") for x in roots)] + if n == "examples.shared": + fl = [(r, src / r) for r in FILES if r.count("/") == 2 and r.startswith("docs/examples/")] + put(n, "any", fl) + for plat in ("linux-x64", "osx-arm64"): + f = src / ("bin-" + plat); f.write_text("exe " + plat + content_tag); f.chmod(0o755) + put("cli", plat, [("bin/ultrafast", f)]) + w = src / "ultrafast_engine-1-py3-none-any.whl" + with zipfile.ZipFile(w, "w") as z: + z.writestr("ultrafast/__init__.py", "wheel" + content_tag) + put("python", "any", [("python/ultrafast_engine-1-py3-none-any.whl", w)]) + p1 = src / "fit.dll"; p1.write_text("fit" + content_tag) + put("plugins.fitting", "any", [("bin/plugins/fitting/fit.dll", p1)]) + p2 = src / "twin.dll"; p2.write_text("twin" + content_tag) + put("plugins.twin", "any", [("bin/plugins/twin/twin.dll", p2)]) + n = src / "native.so"; n.write_text("native") + put("python-native", "linux-x64", [("python/native.bin", n)]) + m = rc.manifest(version, assets, defs) + m["components"] = {k: v for k, v in m["components"].items() if v["assets"]} + (out / "manifest.json").write_text(json.dumps(m)) + return out + + +def run_install(rel, dest, include, exclude=None, platform="linux-x64", **kw): + return inst.install(str(rel), include, exclude, dest, platform, use_pip=False, log=lambda *_: None, **kw) + + +def tree(dest: Path): + return sorted(p.relative_to(dest).as_posix() for p in dest.rglob("*") if p.is_file() and p.name != inst.RECORD) + + +# ---------------------------------------------------------------------------------------------------- resolution +def comps(tmp_path): + return inst.fetch_manifest(str(make_release(tmp_path)))["components"] + + +def test_dotted_names_dependencies_and_excludes(tmp_path): + c = comps(tmp_path) + plan, _ = inst.resolve(c, ["examples"], None, "linux-x64") + assert sorted(plan) == ["cli".replace("cli", "examples.Branched"), "examples.Fibres", "examples.shared", "python"] + plan, _ = inst.resolve(c, ["examples.Branched"], None, "linux-x64") + assert sorted(plan) == ["examples.Branched", "examples.shared", "python"] + plan, _ = inst.resolve(c, ["plugins.twin"], None, "linux-x64") # twin -> fitting -> cli, transitively + assert sorted(plan) == ["cli", "plugins.fitting", "plugins.twin"] + plan, _ = inst.resolve(c, ["plugins"], ["plugins.twin"], "linux-x64") + assert sorted(plan) == ["cli", "plugins.fitting"] + plan, notes = inst.resolve(c, ["tests"], ["python"], "linux-x64") # excluded but needed: kept, with a note + assert "python" in plan and any("excluded" in n for n in notes) + + +def test_all_leaves_out_media_unless_named(tmp_path): + out = make_release(tmp_path) + # media is declared but has no asset in the fake release: add one + m = json.loads((out / "manifest.json").read_text()) + f = tmp_path / "m.png"; f.write_text("png") + m["components"]["media"] = dict(description="m", requires=[], default=False, inAll=False, assets=[dict(platform="any", **rc.write_asset([("docs/examples/a.png", f)], out / "media-1.0.0.tar.gz"))]) + c = m["components"] + plan, _ = inst.resolve(c, ["all"], None, "linux-x64") + assert "media" not in plan and "cli" in plan and "python-native" not in plan + assert "media" in inst.resolve(c, ["all", "media"], None, "linux-x64")[0] + assert "media" not in inst.resolve(c, ["examples.Branched", "media"], ["media"], "linux-x64")[0] + + +def test_platform_choice_and_missing_platform(tmp_path): + c = comps(tmp_path) + assert inst.resolve(c, ["cli"], None, "osx-arm64")[0]["cli"]["asset"]["platform"] == "osx-arm64" + with pytest.raises(inst.InstallError, match="not built for win-x64"): + inst.resolve(c, ["cli"], None, "win-x64") + plan, notes = inst.resolve(c, ["python-native", "tests"], None, "linux-x64") # python-native provides what tests need + assert "python-native" in plan and "python" not in plan + plan, notes = inst.resolve(c, ["all"], ["python-native"], "osx-arm64") # python-native only exists for linux-x64: only a note + assert "python-native" not in plan + with pytest.raises(inst.InstallError, match="cannot be installed together"): + inst.resolve(c, ["python", "python-native"], None, "linux-x64") + + +def test_unknown_name_lists_the_known_ones(tmp_path): + with pytest.raises(inst.InstallError, match="unknown component 'exampels'"): + inst.resolve(comps(tmp_path), ["exampels"], None, "linux-x64") + + +def test_detect_platform_is_a_release_platform(): + assert inst.detect_platform() in ("win-x64", "win-arm64", "linux-x64", "linux-arm64", "osx-arm64", "osx-x64", "win-x86") + + +def test_registry_of_the_real_repository_has_every_example_group(): + d = rc.definitions(REPO, ["fitting", "ao", "twin"]) + groups = {p.name for p in (REPO / "docs" / "examples").iterdir() if p.is_dir() and not p.name.startswith(("_", "."))} + assert {"examples." + g for g in groups} <= set(d) + assert d["plugins.twin"]["requires"] == ["cli", "plugins.fitting"] + assert d["media"]["inAll"] is False and d["examples.Branched"]["requires"] == ["examples.shared", "python"] + plan, _, _, _ = inst.expand(d, ["all"], ["media", "source"]) + assert "cli" in plan and "media" not in plan and "source" not in plan + + +def test_source_components_use_tracked_files_and_leave_out_media_pdfs_and_agent_notes(): + d = rc.definitions(REPO, []) + files = rc.tracked_files(REPO) + ex = {a for a, _ in rc.collect(REPO, "examples.Branched", d, files)} + media = {a for a, _ in rc.collect(REPO, "media", d, files)} + assert ex and media and not (ex & media) + assert all(a.startswith("docs/examples/Branched/") for a in ex) + assert not [a for a in ex | media if a.endswith(("CLAUDE.md", ".pdf"))] + assert all(a.lower().endswith(rc.MEDIA_EXT) for a in media) + shared = {a for a, _ in rc.collect(REPO, "examples.shared", d, files)} + assert "docs/examples/_figures.py" in shared and all(a.count("/") == 2 for a in shared) + src = {a for a, _ in rc.collect(REPO, "source", d, files)} + assert "build/package.py" in src and "CLAUDE.md" in src # the whole repository, agent notes included + assert not [a for a in src if rc.forbidden(a)] + + +# ---------------------------------------------------------------------------------------------------- install / uninstall +def test_install_a_selection_then_add_then_uninstall_leaves_nothing(tmp_path): + rel, dest = make_release(tmp_path), tmp_path / "dest" + run_install(rel, dest, ["python", "tests", "examples.Branched"]) + assert tree(dest) == ["docs/examples/Branched/cavity/run.json", "docs/examples/Branched/make_all.py", "docs/examples/Branched/pump-probe/pp.py", + "docs/examples/Branched/pump-probe/run.json", "docs/examples/_figures.py", "python/ultrafast_engine-1-py3-none-any.whl", "tests/a.py"] + rec = inst.read_record(dest) + assert sorted(rec["components"]) == ["examples.Branched", "examples.shared", "python", "tests"] + run_install(rel, dest, ["cli"]) # a second --include adds + assert (dest / "bin" / "ultrafast").read_text() == "exe linux-x64" + if os.name != "nt": # Windows has no executable bit + assert (dest / "bin" / "ultrafast").stat().st_mode & 0o111 # executable bit survives + inst.uninstall(dest, ["tests"], use_pip=False, log=lambda *_: None) + assert not (dest / "tests").exists() and (dest / "bin" / "ultrafast").exists() + inst.uninstall(dest, None, use_pip=False, log=lambda *_: None) + assert list(dest.iterdir()) == [] # everything it wrote is gone, the record too + + +def test_a_part_of_a_group_travels_with_the_group_helpers(tmp_path): + rel, dest = make_release(tmp_path), tmp_path / "dest" + run_install(rel, dest, ["examples.Branched.pump-probe"]) + assert tree(dest) == ["docs/examples/Branched/make_all.py", "docs/examples/Branched/pump-probe/pp.py", "docs/examples/Branched/pump-probe/run.json", + "docs/examples/_figures.py", "python/ultrafast_engine-1-py3-none-any.whl"] + rec = inst.read_record(dest) + assert rec["components"]["examples.Branched"]["only"] == ["docs/examples/Branched/pump-probe"] + run_install(rel, dest, ["examples.Branched.cavity"]) + assert (dest / "docs/examples/Branched/cavity/run.json").exists() + inst.uninstall(dest, ["examples.Branched.pump-probe"], use_pip=False, log=lambda *_: None) + assert not (dest / "docs/examples/Branched/pump-probe").exists() and (dest / "docs/examples/Branched/cavity/run.json").exists() + + +def test_uninstall_leaves_a_changed_file_and_warns(tmp_path): + rel, dest, msgs = make_release(tmp_path), tmp_path / "dest", [] + run_install(rel, dest, ["tests", "python"]) + (dest / "tests" / "a.py").write_text("my edit") + (dest / "tests" / "mine.txt").write_text("not installed by the tool") + inst.uninstall(dest, None, use_pip=False, log=msgs.append) + assert (dest / "tests" / "a.py").read_text() == "my edit" and (dest / "tests" / "mine.txt").exists() + assert any("changed after installation" in m for m in msgs) + assert not (dest / "python").exists() + + +def test_existing_file_not_ours_is_not_overwritten(tmp_path): + rel, dest = make_release(tmp_path), tmp_path / "dest" + (dest / "tests").mkdir(parents=True) + (dest / "tests" / "a.py").write_text("user file") + run_install(rel, dest, ["tests"]) + assert (dest / "tests" / "a.py").read_text() == "user file" + run_install(rel, dest, ["tests"], force=True) + assert (dest / "tests" / "a.py").read_text() == "a" + + +def test_hash_mismatch_refuses_and_installs_nothing(tmp_path): + rel, dest = make_release(tmp_path), tmp_path / "dest" + asset = rel / "tests-1.0.0.tar.gz" + asset.write_bytes(asset.read_bytes()[:-8] + b"tampered") + with pytest.raises(inst.InstallError, match="SHA-256 mismatch"): + run_install(rel, dest, ["python", "tests"]) + assert not dest.exists() or tree(dest) == [] + + +def test_upgrade_replaces_files_and_drops_stale_ones(tmp_path): + rel1 = make_release(tmp_path, "1.0.0") + dest = tmp_path / "dest" + run_install(rel1, dest, ["tests"]) + rel2 = make_release(tmp_path, "1.1.0", content_tag="v2") + # version 1.1.0 of tests has a different file name + (tmp_path / "src-1.1.0" / "tests" / "a.py").rename(tmp_path / "src-1.1.0" / "tests" / "b.py") + m = json.loads((rel2 / "manifest.json").read_text()) + info = rc.write_asset([("tests/b.py", tmp_path / "src-1.1.0" / "tests" / "b.py")], rel2 / "tests-1.1.0.tar.gz") + m["components"]["tests"]["assets"] = [dict(platform="any", **info)] + (rel2 / "manifest.json").write_text(json.dumps(m)) + run_install(rel2, dest, ["tests"]) + assert tree(dest) == ["python/ultrafast_engine-1-py3-none-any.whl", "tests/b.py"] + assert inst.read_record(dest)["components"]["tests"]["version"] == "1.1.0" + + +def test_zos_api_in_an_archive_is_refused_on_install(tmp_path): + rel, dest = make_release(tmp_path), tmp_path / "dest" + evil = tmp_path / "x"; evil.write_text("x") + # a hand-made archive (the writer would refuse): the installer must still refuse + bad = rel / "tests-1.0.0.tar.gz" + with tarfile.open(bad, "w:gz") as tf: + tf.add(evil, "tests/ZOSAPI.dll") + import hashlib + m = json.loads((rel / "manifest.json").read_text()) + m["components"]["tests"]["assets"][0].update(sha256=hashlib.sha256(bad.read_bytes()).hexdigest(), size=bad.stat().st_size) + (rel / "manifest.json").write_text(json.dumps(m)) + with pytest.raises(inst.InstallError, match="ZOS-API"): + run_install(rel, dest, ["python", "tests"]) + assert not (dest / "tests").exists() + + +def test_unsafe_archive_paths_are_refused(tmp_path): + with pytest.raises(inst.InstallError, match="unsafe"): + inst._safe_member("../outside.txt") + with pytest.raises(inst.InstallError, match="unsafe"): + inst._safe_member("/etc/passwd") + + +def test_pip_command_for_a_wheel_component(tmp_path): + rel, dest, lines = make_release(tmp_path), tmp_path / "dest", [] + inst.install(str(rel), ["python"], None, dest, "linux-x64", dry_run=True, python="py", log=lines.append) + cmd = [l for l in lines if "-m pip" in l][0] + assert "py -m pip install --find-links" in cmd and str(dest / "python") in cmd and "ultrafast-engine[hosted]" in cmd + assert not dest.exists() # a dry run writes nothing + + +# ---------------------------------------------------------------------------------------------------- the ZOS-API gate +def test_asset_writer_refuses_zosapi_files(tmp_path): + f = tmp_path / "ZOSAPI_NetHelper.dll"; f.write_text("x") + with pytest.raises(SystemExit, match="ZOS-API"): + rc.write_asset([("zemax/ZOSAPI_NetHelper.dll", f)], tmp_path / "a.tar.gz") + ok = tmp_path / "Stubs.cs"; ok.write_text("x") + rc.write_asset([("plugins/zemax/tools/ZosApiStubs/Stubs.cs", ok)], tmp_path / "ok.tar.gz") # our own stub sources are allowed + assert rc.forbidden("a/b/zosapi.DLL") and rc.forbidden("ZOSAPI_Interfaces.xml") and not rc.forbidden("ZosApiStubs.csproj") + + +def test_release_gate_finds_a_planted_zosapi_file(tmp_path): + rel = make_release(tmp_path) + assert rc.check_archives([rel]) == [] + assert rc.main(["check-zosapi", str(rel)]) == 0 + planted = tmp_path / "evil.tar.gz" + with tarfile.open(planted, "w:gz") as tf: + info = tarfile.TarInfo("bin/plugins/zemax/ZOSAPI.dll"); info.size = 1 + tf.addfile(info, io.BytesIO(b"x")) + whl = tmp_path / "w.whl" + with zipfile.ZipFile(whl, "w") as z: + z.writestr("ultrafast/bin/ZOSAPI_Interfaces.dll", "x") + shutil.copy(planted, rel / "evil.tar.gz") + assert any("ZOSAPI.dll" in x for x in rc.check_archives([rel])) + assert rc.main(["check-zosapi", str(rel)]) == 1 + assert any("ZOSAPI_Interfaces" in x for x in rc.check_archives([whl])) + + +def test_build_release_with_a_planted_zosapi_in_a_wheel_is_caught(tmp_path): + inner = io.BytesIO() + with zipfile.ZipFile(inner, "w") as z: + z.writestr("ultrafast/bin/hosted/ZOSAPI.dll", "x") + wheel = tmp_path / "x.whl"; wheel.write_bytes(inner.getvalue()) + info = rc.write_asset([("python/x.whl", wheel)], tmp_path / "python.tar.gz") # a wheel is not inspected by the writer ... + assert info["size"] > 0 + assert rc.check_archives([tmp_path / "python.tar.gz"]) # ... the gate looks inside it + + +# ---------------------------------------------------------------------------------------------------- manifest / packager +def test_merge_of_job_manifests(tmp_path): + a = json.loads((make_release(tmp_path / "a") / "manifest.json").read_text()) + b = json.loads(json.dumps(a)) + b["components"]["cli"]["assets"] = [dict(platform="win-x64", file="cli-1.0.0-win-x64.tar.gz", size=1, sha256="0" * 64)] + m = rc.merge([a, b]) + assert [x["platform"] for x in m["components"]["cli"]["assets"]] == ["linux-x64", "osx-arm64", "win-x64"] + b["components"]["cli"]["assets"] = [dict(platform="linux-x64", file="x", size=1, sha256="1" * 64)] + with pytest.raises(SystemExit, match="built twice"): + rc.merge([a, b]) + b["version"] = "9" + with pytest.raises(SystemExit, match="version mismatch"): + rc.merge([a, b]) + + +def test_packager_components_option_and_old_flags_as_aliases(): + a = pkg.parse(["--components", "python", "examples.Branched", "--cli", "--docs"]) + assert a.release_mode and a.components == ["python", "examples.Branched", "cli", "docs"] + assert not pkg.parse(["--cli"]).release_mode + with pytest.raises(SystemExit): + pkg.parse(["--components"]) + + +def test_packager_writes_assets_for_source_components(tmp_path): + out = tmp_path / "out" + assert pkg.main(["--components", "schemas", "examples.Branched", "--out", str(out), "--platform", "linux-x64"]) == 0 + rel = next(out.glob("release-*")) + m = json.loads((rel / "manifest.json").read_text()) + assert {"schemas", "examples.Branched", "examples.shared"} <= set(m["components"]) + assert (rel / "install.py").exists() + dest = tmp_path / "dest" + inst.install(str(rel), ["schemas"], None, dest, "linux-x64", use_pip=False, log=lambda *_: None) + assert (dest / "schemas" / "ufs.schema.json").exists() + inst.uninstall(dest, None, use_pip=False, log=lambda *_: None) + assert list(dest.iterdir()) == [] + + +# ---------------------------------------------------------------------------------------------------- git sparse checkout +@pytest.mark.skipif(shutil.which("git") is None, reason="git not installed") +def test_git_sparse_checkout_and_uninstall_in_a_temporary_repository(tmp_path): + repo = tmp_path / "repo" + for rel, text in FILES.items(): + p = repo / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text) + (repo / "big").mkdir(); (repo / "big" / "x.bin").write_text("not wanted") + + def git(*a): + subprocess.run(["git", "-C", str(repo), "-c", "user.email=t@t", "-c", "user.name=t", *a], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run(["git", "init", "-q", str(repo)], check=True) + git("add", "-A"); git("commit", "-qm", "x"); git("tag", "v1.0.0") + c = _defs() + c["examples.Branched"]["sourcePaths"] = ["docs/examples/Branched"] + dest = tmp_path / "checkout" + inst.install_git(c, ["examples.Branched.pump-probe"], None, dest, "1.0.0", repository=str(repo), log=lambda *_: None) + present = sorted(p.relative_to(dest).as_posix() for p in dest.rglob("*") if p.is_file() and ".git/" not in p.as_posix() and p.name != inst.RECORD) + assert "docs/examples/Branched/pump-probe/run.json" in present + assert "docs/examples/_figures.py" in present and "docs/examples/Branched/make_all.py" in present # cone mode: files of the parent folders + assert not any(p.startswith(("big/", "tests/", "docs/examples/Fibres/", "docs/examples/Branched/cavity/")) for p in present) + inst.install_git(c, ["tests"], None, dest, "1.0.0", repository=str(repo), log=lambda *_: None) # a second --include adds + assert (dest / "tests" / "a.py").exists() + with pytest.raises(inst.InstallError, match="built component"): + inst.install_git(c, ["cli"], None, dest, "1.0.0", repository=str(repo), log=lambda *_: None) + inst.uninstall(dest, ["tests"], use_pip=False, log=lambda *_: None) + assert not (dest / "tests").exists() and (dest / "docs/examples/Branched/pump-probe/run.json").exists() + inst.uninstall(dest, None, use_pip=False, log=lambda *_: None) + assert not dest.exists() diff --git a/schemas/run.schema.json b/schemas/run.schema.json index 8ab1723..d0a77eb 100644 --- a/schemas/run.schema.json +++ b/schemas/run.schema.json @@ -502,7 +502,7 @@ }, "snapCarrier": { "type": "boolean", - "description": "Put the carrier on the frequency grid Default: true." + "description": "Put the carrier on the frequency grid. A cw source is not snapped (its line sits on centerWavelengthM) unless this is written Default: true." }, "spectralMargin": { "type": "number", @@ -575,6 +575,14 @@ "type": "boolean", "description": "Add air dispersion to air paths quoted relative to air Default: true." }, + "strictMaterialsAllow": { + "type": "array", + "description": "Catalogue quantities that strictMaterials accepts although they are not verified, by catalogue path (e.g. \"CS2.n2\") as listed in the error and in summary.json unverifiedInputs; matching is case-insensitive and an entry name such as \"CS2\" accepts all its quantities" + }, + "strictMaterials": { + "type": "boolean", + "description": "Refuse a run that uses approximate or unverified material-catalogue values (the compile error lists them; override each, or give a verified value in your own catalogue file). The environment variable ULTRAFAST_STRICT_MATERIALS=1 does the same Default: false." + }, "polarization": { "type": "string", "enum": ["jones", "scalar"], diff --git a/schemas/ufs.schema.json b/schemas/ufs.schema.json index 6d15ef1..faa7316 100644 --- a/schemas/ufs.schema.json +++ b/schemas/ufs.schema.json @@ -855,7 +855,7 @@ "description": "Spatial gain" }, "thermalLens": { - "description": "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens)" + "description": "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens)" }, "slices": { "type": "integer", @@ -911,6 +911,9 @@ "type": "integer", "description": "ASE random seed" }, + "aseModel": { + "description": "true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase) Default: false." + }, "temperatureK": { "type": "number", "description": "Crystal temperature [K]" @@ -5022,7 +5025,7 @@ "description": "Spatial gain" }, "thermalLens": { - "description": "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens)" + "description": "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens)" }, "slices": { "type": "integer", @@ -5078,6 +5081,9 @@ "type": "integer", "description": "ASE random seed" }, + "aseModel": { + "description": "true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase) Default: false." + }, "temperatureK": { "type": "number", "description": "Crystal temperature [K]" diff --git a/src/UltrafastEngine.Cli/Program.cs b/src/UltrafastEngine.Cli/Program.cs index 7743711..a5c0c4c 100644 --- a/src/UltrafastEngine.Cli/Program.cs +++ b/src/UltrafastEngine.Cli/Program.cs @@ -81,7 +81,7 @@ public static int Main(string[] args) case "materials": return MaterialsCommand(args.Skip(1).ToArray()); case "reference": { - string md = Schema.ReferenceGenerator.Markdown(), o = Opt(args, "-o"); + string md = Positional(args.Skip(1).ToArray()).FirstOrDefault() == "materials" ? Materials.CatalogueReference.Markdown() : Schema.ReferenceGenerator.Markdown(), o = Opt(args, "-o"); if (o == null) Console.WriteLine(md); else { File.WriteAllText(o, md); Console.WriteLine("wrote " + o); } return 0; } diff --git a/src/UltrafastEngine.Core/Crystals/Crystal.cs b/src/UltrafastEngine.Core/Crystals/Crystal.cs index 7702f97..73fb70f 100644 --- a/src/UltrafastEngine.Core/Crystals/Crystal.cs +++ b/src/UltrafastEngine.Core/Crystals/Crystal.cs @@ -113,6 +113,8 @@ public sealed class CrystalModel public string References { get; set; } public List Notes { get; } = new List(); public bool TemperatureDependent { get; set; } + /// The catalogue quantities this crystal takes its numbers from ("indexX", "indexZ", "d", "n2", ...); a user override removes the quantity. + public Dictionary Used { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public bool IsUniaxial => OpticalClass == "uniaxial"; /// Uniaxial: true if ne < no (BBO, KDP, LiNbO3); then o = slow, e = fast. @@ -246,123 +248,42 @@ public string Describe() /// Built-in chi(2) crystals and user-defined ones from JSON. public static class CrystalLibrary { - public static readonly string[] Names = { "BBO", "LBO", "KTP", "KDP", "LiNbO3", "MgO:LiNbO3", "BiBO", "AgGaSe2" }; + /// The built-in crystals, in catalogue order (Data/materials/crystals). + public static string[] Names => Catalogue.Entries("crystal").Select(e => e.Id).ToArray(); - private static IRefractiveIndex F2(string reference, double lo, double hi, params double[] c) => new RiiFormulaIndex(2, c, lo, hi, reference); - private static IRefractiveIndex F4(string reference, double lo, double hi, params double[] c) => new RiiFormulaIndex(4, c, lo, hi, reference); - private static Dictionary Dpm(params (string, double)[] v) => v.ToDictionary(x => x.Item1, x => x.Item2); + private static Dictionary Dpm(JsonObject o) => o.ToDictionary(kv => kv.Key, kv => kv.Value.AsDouble()); + /// The index model of an "index*" quantity of a crystal (refractiveindex.info formula 2 or 4, or the temperature-dependent LiNbO3 form). + private static IRefractiveIndex Index(CatalogueQuantity q, double temperatureC) + { + var vr = q.Array("validRangeUm"); string cite = q.Text("citation"); + switch (q.Text("model")) + { + case "riiFormula2": return new RiiFormulaIndex(2, q.Array("coefficients"), vr[0], vr[1], cite); + case "riiFormula4": return new RiiFormulaIndex(4, q.Array("coefficients"), vr[0], vr[1], cite); + case "jundt": return new LithiumNiobateIndex(q.Array("a"), q.Array("b"), temperatureC, cite); + default: throw new ModelException("catalogue " + q.Path + ": unknown crystal index model \"" + q.Text("model") + "\"."); + } + } + + /// A built-in chi(2) crystal from the catalogue: principal indices, d tensor, n2, validity range, each with its source and status. public static CrystalModel Get(string name, double temperatureC = double.NaN) { string key = (name ?? "").Trim().ToLowerInvariant().Replace(" ", ""); - const string tam = "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018)"; - const string kato = "Kato & Takaoka, Appl. Opt. 41, 5040 (2002)"; - const string kato94 = "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)"; - const string zern = "Zernike, JOSA 54, 1215 (1964)"; - const string zel = "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997)"; - const string ume = "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007)"; - const string katoAgs = "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato)"; + var e = Catalogue.Find(key, "crystal") ?? throw new ModelException("unknown crystal \"" + name + "\". Built-in crystals: " + string.Join(", ", Names) + ". Define your own with \"crystal\": {\"class\": \"uniaxial\", \"no\": ..., \"ne\": ..., \"d\": {...}}."); double T = double.IsNaN(temperatureC) ? 20 : temperatureC; - CrystalModel c; - switch (key) - { - case "bbo": case "b-bab2o4": case "bab2o4": case "beta-bbo": - c = new CrystalModel - { - Name = "BBO", OpticalClass = "uniaxial", PointGroup = "3m", - Nx = F2(tam, 0.188, 5.2, 0, 0.90291, 0.003926, 0.83155, 0.018786, 0.76536, 60.01), - Nz = F2(tam, 0.188, 5.2, 0, 1.151075, 0.007142, 0.21803, 0.02259, 0.656, 263), - D = CrystalModel.Tensor("3m", Dpm(("d22", 2.2), ("d31", 0.04))), N2 = 2.8e-20, - ValidMinUm = 0.188, ValidMaxUm = 5.2, - References = "indices: " + tam + "; d22 = 2.2, d31 = 0.04 pm/V (Eckardt et al., IEEE JQE 26, 922 (1990)); n2: DeSalvo et al., IEEE JQE 32, 1324 (1996)" - }; - break; - case "lbo": case "lib3o5": - c = new CrystalModel - { - Name = "LBO", OpticalClass = "biaxial", PointGroup = "mm2 (polar axis = Y)", - // Kato (1994): n^2 = A + B/(l^2 - C) - D l^2 (+ E l^4 - F l^6), l in um. Reproduces the handbook - // XY-plane type-I SHG angles (phi = 11.6 deg at 1064 nm, 13.8 deg at 1030 nm, 31.7 deg at 800 nm); - // the Chen/Hanson-Dick fit on refractiveindex.info gives 11.8 / 14.6 / 34.3 deg. - Nx = F4(kato94, 0.19, 2.6, 2.4542, 0.01125, 0, 0.01135, 1, 0, 0, 0, 1, -0.01388, 2), - Ny = F4(kato94, 0.19, 2.6, 2.5390, 0.01277, 0, 0.01189, 1, 0, 0, 0, 1, -0.01849, 2, 4.3025e-5, 4, -2.9131e-5, 6), - Nz = F4(kato94, 0.19, 2.6, 2.5865, 0.01310, 0, 0.01223, 1, 0, 0, 0, 1, -0.01862, 2, 4.5778e-5, 4, -3.2526e-5, 6), - D = CrystalModel.Tensor("mm2:y", Dpm(("d31", -0.67), ("d32", 0.85), ("d33", 0.04))), N2 = 1.9e-20, - ValidMinUm = 0.19, ValidMaxUm = 2.6, - References = "indices: " + kato94 + " (room temperature); d31 = -0.67, d32 = 0.85, d33 = 0.04 pm/V (Dmitriev, Gurzadyan & Nikogosyan, Handbook of Nonlinear Optical Crystals); n2 ~ 1.9e-20 m^2/W (approximate)" - }; - c.Notes.Add("LBO indices are room-temperature (no thermal model): non-critical phase matching by temperature is not modelled"); - break; - case "ktp": case "ktiopo4": case "ppktp": - c = new CrystalModel - { - Name = key == "ppktp" ? "PPKTP" : "KTP", OpticalClass = "biaxial", PointGroup = "mm2", - Nx = F4(kato, 0.43, 3.54, 3.29100, 0.04140, 0, 0.03978, 1, 9.35522, 0, 31.45571, 1), - Ny = F4(kato, 0.43, 3.54, 3.45018, 0.04341, 0, 0.04597, 1, 16.98825, 0, 39.43799, 1), - Nz = F4(kato, 0.43, 3.54, 4.59423, 0.06206, 0, 0.04763, 1, 110.80672, 0, 86.12171, 1), - D = CrystalModel.Tensor("mm2", Dpm(("d31", 1.95), ("d32", 3.9), ("d33", 15.3))), N2 = 2.2e-19, - ValidMinUm = 0.43, ValidMaxUm = 3.54, - References = "indices: " + kato + " (20 C); d31 = 1.95, d32 = 3.9, d33 = 15.3 pm/V at 1064 nm (Vanherzeele & Bierlein, Opt. Lett. 17, 982 (1992)); n2: DeSalvo et al. (1996), E||z" - }; - break; - case "kdp": case "kh2po4": - c = new CrystalModel - { - Name = "KDP", OpticalClass = "uniaxial", PointGroup = "-42m", - Nx = F4(zern, 0.2138, 1.529, 2.259276, 13.00522, 2, 400, 1, 0.01008956, 0, 0.0129426, 1), - Nz = F4(zern, 0.2138, 1.529, 2.132668, 3.2279924, 2, 400, 1, 0.008637494, 0, 0.0122810, 1), - D = CrystalModel.Tensor("-42m", Dpm(("d36", 0.39))), N2 = 2.0e-20, - ValidMinUm = 0.2138, ValidMaxUm = 1.529, - References = "indices: " + zern + "; d36 = 0.39 pm/V (Eckardt et al. 1990); n2: Adair et al. (refractiveindex.info)" - }; - break; - case "linbo3": case "ln": case "cln": case "ppln": - c = new CrystalModel - { - Name = key == "ppln" ? "PPLN (congruent)" : "LiNbO3 (congruent)", OpticalClass = "uniaxial", PointGroup = "3m", TemperatureDependent = true, TemperatureC = T, - Nx = F2(zel, 0.4, 5.0, 0, 2.6734, 0.01764, 1.2290, 0.05914, 12.614, 474.60), - Nz = new LithiumNiobateIndex(new[] { 5.35583, 0.100473, 0.20692, 100.0, 11.34927, 1.5334e-2 }, new[] { 4.629e-7, 3.862e-8, -0.89e-8, 2.657e-5 }, T, "Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray"), - D = CrystalModel.Tensor("3m", Dpm(("d22", 2.1), ("d31", 4.6), ("d33", 25.0))), N2 = 9.3e-20, - ValidMinUm = 0.4, ValidMaxUm = 5.0, - References = "e: Jundt (1997) (temperature dependent); o: " + zel + " (21 C); d33 = 25, d31 = 4.6, d22 = 2.1 pm/V (Shoji et al., JOSA B 14, 2268 (1997)); n2: DeSalvo et al. (1996)" - }; - break; - case "mgo:linbo3": case "mgln": case "mgo:ln": case "mgo:ppln": case "ppmgln": case "mgo:cln": - c = new CrystalModel - { - Name = "MgO:LiNbO3 (5 mol%)", OpticalClass = "uniaxial", PointGroup = "3m", TemperatureDependent = true, TemperatureC = T, - Nx = F2(zel, 0.4, 5.0, 0, 2.6734, 0.01764, 1.2290, 0.05914, 12.614, 474.60), - Nz = new LithiumNiobateIndex(new[] { 5.756, 0.0983, 0.2020, 189.32, 12.52, 1.32e-2 }, new[] { 2.860e-6, 4.700e-8, 6.113e-8, 1.516e-4 }, T, "Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray"), - D = CrystalModel.Tensor("3m", Dpm(("d22", 2.1), ("d31", 4.4), ("d33", 25.0))), N2 = 9.3e-20, - ValidMinUm = 0.5, ValidMaxUm = 4.0, - References = "e: Gayer et al. (2008) (temperature dependent; the room-temperature coefficients are verified, the temperature coefficient b4 = 1.516e-4 could not be cross-checked -- pyNLO uses 1.526e-5); o: congruent LiNbO3, " + zel + "; d33 = 25 pm/V" - }; - c.Notes.Add("MgO:LiNbO3: the o-ray index is that of congruent LiNbO3 (only e-ray data for the doped crystal are built in); type-0 (eee) QPM is unaffected"); - break; - case "bibo": case "bib3o6": - c = new CrystalModel - { - Name = "BiBO", OpticalClass = "biaxial", PointGroup = null, - Nx = F4(ume, 0.48, 3.1, 3.07403, 0.03231, 0, 0.03163, 1, 0, 0, 0, 1, -0.013376, 2), - Ny = F4(ume, 0.48, 3.1, 3.16940, 0.03717, 0, 0.03483, 1, 0, 0, 0, 1, -0.01827, 2), - Nz = F4(ume, 0.48, 3.1, 3.6545, 0.05112, 0, 0.03713, 1, 0, 0, 0, 1, -0.02261, 2), - D = null, N2 = 7.65e-20, ValidMinUm = 0.48, ValidMaxUm = 3.1, - References = "indices (n_alpha, n_beta, n_gamma as x, y, z): " + ume + "; n2: Kabacinski et al. (refractiveindex.info); d tensor not built in (monoclinic): give deffPmPerV" - }; - break; - case "aggase2": - c = new CrystalModel - { - Name = "AgGaSe2", OpticalClass = "uniaxial", PointGroup = "-42m", - Nx = F4(katoAgs, 0.81, 18, 11.62264, 0.43221, 0, 0.15597, 1, 18868.52, 0, 3953.34, 1), - Nz = F4(katoAgs, 0.81, 18, 11.33168, 0.45951, 0, 0.21284, 1, 17816.81, 0, 3828.78, 1), - D = CrystalModel.Tensor("-42m", Dpm(("d36", 33))), N2 = 3.5e-18, ValidMinUm = 0.81, ValidMaxUm = 18, - References = "indices: " + katoAgs + "; d36 = 33 pm/V; n2 approximate" - }; - break; - default: - throw new ModelException("unknown crystal \"" + name + "\". Built-in crystals: " + string.Join(", ", Names) + ". Define your own with \"crystal\": {\"class\": \"uniaxial\", \"no\": ..., \"ne\": ..., \"d\": {...}}."); - } + bool tdep = e.Raw.GetBool("temperatureDependent", false); + string display = e.Raw.GetObject("aliasDisplayNames")?.GetString(key) ?? e.Raw.GetString("displayName", e.Id); + var c = new CrystalModel { Name = display, OpticalClass = e.Raw.GetString("opticalClass"), TemperatureDependent = tdep, References = e.Raw.GetString("references") }; + if (tdep) c.TemperatureC = T; + c.Nx = Index(e.Q("indexX"), T); c.Nz = Index(e.Q("indexZ"), T); + if (e.Has("indexY")) c.Ny = Index(e.Q("indexY"), T); + var d = e.TryQ("d"); + if (d != null) { c.PointGroup = d.Text("pointGroup"); c.D = CrystalModel.Tensor(d.Text("tensor"), Dpm(d.Json.GetObject("dPmPerV"))); } + c.N2 = e.Q("n2").Number("valueM2PerW"); + var v = e.Q("validity").Array("validRangeUm"); c.ValidMinUm = v[0]; c.ValidMaxUm = v[1]; + if (!string.IsNullOrEmpty(e.Notes)) c.Notes.Add(e.Notes); + foreach (var q in e.Quantities.Values) c.Used[q.Name] = q; if (c.Ny == null) c.Ny = c.Nx; if (!c.TemperatureDependent && !double.IsNaN(temperatureC) && Math.Abs(temperatureC - 20) > 5) c.Notes.Add(c.Name + ": the built-in indices are not temperature dependent; temperatureC = " + temperatureC.ToString("G4", CultureInfo.InvariantCulture) + " is ignored"); @@ -381,11 +302,11 @@ public static CrystalModel FromJson(JsonObject o, ModelRegistry reg, double temp if (o.Has("name")) c.Name = o.GetString("name"); if (o.Has("class")) c.OpticalClass = o.GetString("class").ToLowerInvariant(); IRefractiveIndex Ix(string key) => reg.Create("index", ModelSpec.FromJson(o[key], ModelSource.SystemData, origin + "." + key)); - if (o.Has("no")) { c.Nx = Ix("no"); c.Ny = c.Nx; } - if (o.Has("ne")) c.Nz = Ix("ne"); - if (o.Has("nx")) c.Nx = Ix("nx"); - if (o.Has("ny")) c.Ny = Ix("ny"); - if (o.Has("nz")) c.Nz = Ix("nz"); + if (o.Has("no")) { c.Nx = Ix("no"); c.Ny = c.Nx; c.Used.Remove("indexX"); c.Used.Remove("indexY"); } + if (o.Has("ne")) { c.Nz = Ix("ne"); c.Used.Remove("indexZ"); } + if (o.Has("nx")) { c.Nx = Ix("nx"); c.Used.Remove("indexX"); } + if (o.Has("ny")) { c.Ny = Ix("ny"); c.Used.Remove("indexY"); } + if (o.Has("nz")) { c.Nz = Ix("nz"); c.Used.Remove("indexZ"); } if (c.Ny == null) c.Ny = c.Nx; if (c.Nx == null || c.Nz == null) throw new ModelException(origin + ": a user crystal needs \"no\" and \"ne\" (uniaxial) or \"nx\", \"ny\", \"nz\" (biaxial) index models."); if (o.Has("d")) @@ -394,10 +315,10 @@ public static CrystalModel FromJson(JsonObject o, ModelRegistry reg, double temp var map = new Dictionary(); foreach (var kv in dObj) map[kv.Key.ToLowerInvariant()] = kv.Value.AsDouble(); c.PointGroup = o.GetString("pointGroup", "general"); - c.D = CrystalModel.Tensor(c.PointGroup, map); + c.D = CrystalModel.Tensor(c.PointGroup, map); c.Used.Remove("d"); } - if (o.Has("deffPmPerV")) c.DeffOverrideMPerV = o.GetDouble("deffPmPerV", 0) * 1e-12; - if (o.Has("n2")) c.N2 = o.GetDouble("n2", 0); + if (o.Has("deffPmPerV")) { c.DeffOverrideMPerV = o.GetDouble("deffPmPerV", 0) * 1e-12; c.Used.Remove("d"); } + if (o.Has("n2")) { c.N2 = o.GetDouble("n2", 0); c.Used.Remove("n2"); } if (o.Has("absorption")) c.Absorption = reg.Create("attenuation", ModelSpec.FromJson(o["absorption"], ModelSource.SystemData, origin + ".absorption")); if (o.Has("references")) c.References = o.GetString("references"); return c; diff --git a/src/UltrafastEngine.Core/Crystals/CrystalElement.cs b/src/UltrafastEngine.Core/Crystals/CrystalElement.cs index 9c25f51..275239c 100644 --- a/src/UltrafastEngine.Core/Crystals/CrystalElement.cs +++ b/src/UltrafastEngine.Core/Crystals/CrystalElement.cs @@ -753,7 +753,7 @@ public static void Compile(CompilerServices c, SystemElement e, CompiledElement el.Output = j.GetString("output", null); if (j.Has("record")) el.Record = new HashSet(j["record"].AsStringArray(), StringComparer.OrdinalIgnoreCase); el.DeffOverride = j.Has("deffPmPerV") ? c.Num(e, "deffPmPerV", 0) * 1e-12 : double.NaN; - if (!double.IsNaN(el.DeffOverride)) crystal.DeffOverrideMPerV = el.DeffOverride; + if (!double.IsNaN(el.DeffOverride)) { crystal.DeffOverrideMPerV = el.DeffOverride; crystal.Used.Remove("d"); } el.SeedNoise = j.GetBool("seedNoise", false); el.SpectralMargin = c.Num(e, "spectralMargin", 2.5); if (!(el.SpectralMargin >= 1.2)) throw new JsonException("crystal '" + e.Id + "': spectralMargin must be >= 1.2"); @@ -945,6 +945,7 @@ public static void Compile(CompilerServices c, SystemElement e, CompiledElement } ce.Active = el; c.Provenance.Record("element:" + e.Id, "crystal", crystal.Describe() + "; " + crystal.References, ModelSource.MaterialDefault, "crystal library"); + foreach (var q in crystal.Used.Values) c.Provenance.UseCatalogue("element:" + e.Id, q.Name, q, crystal.Name, c.Notes); c.Provenance.Record("element:" + e.Id, "cut", string.Format(CultureInfo.InvariantCulture, "theta {0:F4} deg, phi {1:F3} deg{2}, roles {3}", el.ThetaRad * 180 / Math.PI, el.PhiRad * 180 / Math.PI, el.NoncollinearRad != 0 ? string.Format(CultureInfo.InvariantCulture, ", non-collinear {0:F3} deg", el.NoncollinearRad * 180 / Math.PI) : "", string.Join("/", el.Roles)), autoCut ? ModelSource.EngineDefault : ModelSource.SystemData, autoCut ? "phase matched at the nominal wavelengths (cut \"auto\")" : "system file"); diff --git a/src/UltrafastEngine.Core/Data/materials/README.md b/src/UltrafastEngine.Core/Data/materials/README.md new file mode 100644 index 0000000..8ca1133 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/README.md @@ -0,0 +1,23 @@ +# Material catalogue + +One JSON file (comments allowed) per material, gas or Raman model. Embedded in `UltrafastEngine.Core` (`Catalogue.cs` loads it); the generated, readable +form is `docs/reference/materials-catalogue.md`. User files in `~/.ultrafast/materials` replace single quantities (see `docs/reference/materials.md`). + +``` +solids/.json kind "solid": index, n2, raman (a reference to raman/), ramanFraction, kerrAnisotropy, ionization +gases/.json kind "gas": index, n2Electronic, ionization, rotation, dephasing (air also: composition) +raman/.json kind "raman": response (model + parameters), ramanFraction, anisotropicFraction +gain/.json kind "gain": doping, lifetime, zeroLine, thermal, stress, emission, absorption (bands in cm^2) +crystals/.json kind "crystal": indexX / indexY / indexZ (formula + coefficients), d (pm/V), n2, validity; entry-level displayName, opticalClass, references + (Air, in gases/, also holds the constants of Ciddor's formulas) +``` + +Rules (a test, `CatalogueTests.CatalogueFiles_HaveSourceStatusAndUnitsInTheirKeys`, enforces them): + +* every quantity has a `source` and a `status` (`verified` | `approximate` | `unverified`); one that is not verified also states an uncertainty; +* a number has its unit in the key name (`valueM2PerW`, `tau1S`, `ionizationEv`); coefficients keep the units of their source; +* `verified` means the number was read from the cited source (or its database page) and compared; write the comparison into `verification`; +* never put licensed paper text or figures here, only the extracted numbers and the citation. + +Changing a number: edit the file, run `/regen` (the reference page), and re-pin the values on purpose with +`ULTRAFAST_WRITE_SNAPSHOT=1 dotnet tests/UltrafastEngine.Tests/bin/Release/net10.0/UltrafastEngine.Tests.dll CatalogueSnapshot`; say in the commit which value moved and why. diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/AgGaSe2.json b/src/UltrafastEngine.Core/Data/materials/crystals/AgGaSe2.json new file mode 100644 index 0000000..b4586fd --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/AgGaSe2.json @@ -0,0 +1,66 @@ +{ + "file": "AgGaSe2", + "id": "AgGaSe2", + "kind": "crystal", + "listOrder": 8, + "aliases": [], + "displayName": "AgGaSe2", + "opticalClass": "uniaxial", + "temperatureDependent": false, + "references": "indices: Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato); d36 = 33 pm/V; n2 approximate", + "description": "silver gallium selenide, negative uniaxial (mid-infrared)", + "quantities": { + "indexX": { + "model": "riiFormula4", + "coefficients": [11.62264, 0.43221, 0, 0.15597, 1, 18868.52, 0, 3953.34, 1], + "validRangeUm": [0.81, 18], + "citation": "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (refractiveindex.info AgGaSe2/Kato-o)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page AgGaSe2_Kato-o (formula 4, 9 coefficients)" + }, + "indexZ": { + "model": "riiFormula4", + "coefficients": [11.33168, 0.45951, 0, 0.21284, 1, 17816.81, 0, 3828.78, 1], + "validRangeUm": [0.81, 18], + "citation": "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato) (refractiveindex.info AgGaSe2/Kato-e)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page AgGaSe2_Kato-e (formula 4, 9 coefficients)" + }, + "d": { + "pointGroup": "-42m", + "tensor": "-42m", + "dPmPerV": { + "d36": 33 + }, + "source": "d36 = 33 pm/V of the original code; no source recorded", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 3.5e-18, + "source": "approximate value of the original code (no source recorded)", + "status": "unverified", + "relativeUncertainty": 1.0, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.81, 18], + "source": "Kato & Shirahata, Jpn. J. Appl. Phys. 35, 4645 (1996) (refractiveindex.info: Kato)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/BBO.json b/src/UltrafastEngine.Core/Data/materials/crystals/BBO.json new file mode 100644 index 0000000..7e98ad8 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/BBO.json @@ -0,0 +1,71 @@ +{ + "file": "BBO", + "id": "BBO", + "kind": "crystal", + "listOrder": 1, + "aliases": [ + "b-bab2o4", + "bab2o4", + "beta-bbo" + ], + "displayName": "BBO", + "opticalClass": "uniaxial", + "temperatureDependent": false, + "references": "indices: Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018); d22 = 2.2, d31 = 0.04 pm/V (Eckardt et al., IEEE JQE 26, 922 (1990)); n2: DeSalvo et al., IEEE JQE 32, 1324 (1996)", + "description": "beta-barium borate, negative uniaxial", + "quantities": { + "indexX": { + "model": "riiFormula2", + "coefficients": [0, 0.90291, 0.003926, 0.83155, 0.018786, 0.76536, 60.01], + "validRangeUm": [0.188, 5.2], + "citation": "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (refractiveindex.info BaB2O4/Tamosauskas-o)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page BaB2O4_Tamosauskas-o (formula 2, 7 coefficients)" + }, + "indexZ": { + "model": "riiFormula2", + "coefficients": [0, 1.151075, 0.007142, 0.21803, 0.02259, 0.656, 263], + "validRangeUm": [0.188, 5.2], + "citation": "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018) (refractiveindex.info BaB2O4/Tamosauskas-e)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page BaB2O4_Tamosauskas-e (formula 2, 7 coefficients)" + }, + "d": { + "pointGroup": "3m", + "tensor": "3m", + "dPmPerV": { + "d22": 2.2, + "d31": 0.04 + }, + "source": "Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 2.8e-20, + "source": "DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.188, 5.2], + "source": "Tamosauskas et al., Opt. Mater. Express 8, 1410 (2018)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/BiBO.json b/src/UltrafastEngine.Core/Data/materials/crystals/BiBO.json new file mode 100644 index 0000000..adfbeb4 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/BiBO.json @@ -0,0 +1,67 @@ +{ + "file": "BiBO", + "id": "BiBO", + "kind": "crystal", + "listOrder": 7, + "aliases": [ + "bib3o6" + ], + "displayName": "BiBO", + "opticalClass": "biaxial", + "temperatureDependent": false, + "references": "indices (n_alpha, n_beta, n_gamma as x, y, z): Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007); n2: Kabacinski et al. (refractiveindex.info); d tensor not built in (monoclinic): give deffPmPerV", + "description": "bismuth triborate, biaxial (monoclinic: no d tensor built in)", + "quantities": { + "indexX": { + "model": "riiFormula4", + "coefficients": [3.07403, 0.03231, 0, 0.03163, 1, 0, 0, 0, 1, -0.013376, 2], + "validRangeUm": [0.48, 3.1], + "citation": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-alpha)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page BiB3O6_Umemura-alpha (formula 4, 11 coefficients)" + }, + "indexY": { + "model": "riiFormula4", + "coefficients": [3.1694, 0.03717, 0, 0.03483, 1, 0, 0, 0, 1, -0.01827, 2], + "validRangeUm": [0.48, 3.1], + "citation": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-beta)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page BiB3O6_Umemura-beta (formula 4, 11 coefficients)" + }, + "indexZ": { + "model": "riiFormula4", + "coefficients": [3.6545, 0.05112, 0, 0.03713, 1, 0, 0, 0, 1, -0.02261, 2], + "validRangeUm": [0.48, 3.1], + "citation": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007) (refractiveindex.info BiB3O6/Umemura-gamma)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page BiB3O6_Umemura-gamma (formula 4, 11 coefficients)" + }, + "n2": { + "valueM2PerW": 7.65e-20, + "source": "Kabacinski et al. (via refractiveindex.info), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.48, 3.1], + "source": "Umemura, Miyata & Kato, Opt. Mater. 30, 532 (2007)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/KDP.json b/src/UltrafastEngine.Core/Data/materials/crystals/KDP.json new file mode 100644 index 0000000..2573a8c --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/KDP.json @@ -0,0 +1,68 @@ +{ + "file": "KDP", + "id": "KDP", + "kind": "crystal", + "listOrder": 4, + "aliases": [ + "kh2po4" + ], + "displayName": "KDP", + "opticalClass": "uniaxial", + "temperatureDependent": false, + "references": "indices: Zernike, JOSA 54, 1215 (1964); d36 = 0.39 pm/V (Eckardt et al. 1990); n2: Adair et al. (refractiveindex.info)", + "description": "potassium dihydrogen phosphate, negative uniaxial", + "quantities": { + "indexX": { + "model": "riiFormula4", + "coefficients": [2.259276, 13.00522, 2, 400, 1, 0.01008956, 0, 0.0129426, 1], + "validRangeUm": [0.2138, 1.529], + "citation": "Zernike, JOSA 54, 1215 (1964)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Zernike, JOSA 54, 1215 (1964) (refractiveindex.info KH2PO4/Zernike-o)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page KH2PO4_Zernike-o (formula 4, 9 coefficients)" + }, + "indexZ": { + "model": "riiFormula4", + "coefficients": [2.132668, 3.2279924, 2, 400, 1, 0.008637494, 0, 0.012281, 1], + "validRangeUm": [0.2138, 1.529], + "citation": "Zernike, JOSA 54, 1215 (1964)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Zernike, JOSA 54, 1215 (1964) (refractiveindex.info KH2PO4/Zernike-e)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page KH2PO4_Zernike-e (formula 4, 9 coefficients)" + }, + "d": { + "pointGroup": "-42m", + "tensor": "-42m", + "dPmPerV": { + "d36": 0.39 + }, + "source": "Eckardt et al., IEEE JQE 26, 922 (1990), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 2e-20, + "source": "Adair et al. (via refractiveindex.info), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.2138, 1.529], + "source": "Zernike, JOSA 54, 1215 (1964)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/KTP.json b/src/UltrafastEngine.Core/Data/materials/crystals/KTP.json new file mode 100644 index 0000000..3a3b355 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/KTP.json @@ -0,0 +1,86 @@ +{ + "file": "KTP", + "id": "KTP", + "kind": "crystal", + "listOrder": 3, + "aliases": [ + "ktiopo4", + "ppktp" + ], + "aliasDisplayNames": { + "ppktp": "PPKTP" + }, + "displayName": "KTP", + "opticalClass": "biaxial", + "temperatureDependent": false, + "references": "indices: Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (20 C); d31 = 1.95, d32 = 3.9, d33 = 15.3 pm/V at 1064 nm (Vanherzeele & Bierlein, Opt. Lett. 17, 982 (1992)); n2: DeSalvo et al. (1996), E||z", + "description": "potassium titanyl phosphate, biaxial (the aliases ppktp name the periodically poled form)", + "quantities": { + "indexX": { + "model": "riiFormula4", + "coefficients": [3.291, 0.0414, 0, 0.03978, 1, 9.35522, 0, 31.45571, 1], + "validRangeUm": [0.43, 3.54], + "citation": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-alpha)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page KTiOPO4_Kato-alpha (formula 4, 9 coefficients)" + }, + "indexY": { + "model": "riiFormula4", + "coefficients": [3.45018, 0.04341, 0, 0.04597, 1, 16.98825, 0, 39.43799, 1], + "validRangeUm": [0.43, 3.54], + "citation": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-beta)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page KTiOPO4_Kato-beta (formula 4, 9 coefficients)" + }, + "indexZ": { + "model": "riiFormula4", + "coefficients": [4.59423, 0.06206, 0, 0.04763, 1, 110.80672, 0, 86.12171, 1], + "validRangeUm": [0.43, 3.54], + "citation": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002) (refractiveindex.info KTiOPO4/Kato-gamma)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page KTiOPO4_Kato-gamma (formula 4, 9 coefficients)" + }, + "d": { + "pointGroup": "mm2", + "tensor": "mm2", + "dPmPerV": { + "d31": 1.95, + "d32": 3.9, + "d33": 15.3 + }, + "source": "Vanherzeele & Bierlein, Opt. Lett. 17, 982 (1992), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 2.2e-19, + "source": "DeSalvo et al., IEEE JQE 32, 1324 (1996), E||z, as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.43, 3.54], + "source": "Kato & Takaoka, Appl. Opt. 41, 5040 (2002)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/LBO.json b/src/UltrafastEngine.Core/Data/materials/crystals/LBO.json new file mode 100644 index 0000000..5d63a28 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/LBO.json @@ -0,0 +1,83 @@ +{ + "file": "LBO", + "id": "LBO", + "kind": "crystal", + "listOrder": 2, + "aliases": [ + "lib3o5" + ], + "displayName": "LBO", + "opticalClass": "biaxial", + "temperatureDependent": false, + "notes": "LBO indices are room-temperature (no thermal model): non-critical phase matching by temperature is not modelled", + "references": "indices: Kato, IEEE J. Quantum Electron. 30, 2950 (1994) (room temperature); d31 = -0.67, d32 = 0.85, d33 = 0.04 pm/V (Dmitriev, Gurzadyan & Nikogosyan, Handbook of Nonlinear Optical Crystals); n2 ~ 1.9e-20 m^2/W (approximate)", + "description": "lithium triborate, biaxial; the Kato (1994) set reproduces the handbook XY-plane type-I SHG angles (phi = 11.6 deg at 1064 nm, 13.8 deg at 1030 nm, 31.7 deg at 800 nm), the Chen / Hanson-Dick fit on refractiveindex.info gives 11.8 / 14.6 / 34.3 deg", + "quantities": { + "indexX": { + "model": "riiFormula4", + "coefficients": [2.4542, 0.01125, 0, 0.01135, 1, 0, 0, 0, 1, -0.01388, 2], + "validRangeUm": [0.19, 2.6], + "citation": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "status": "unverified", + "uncertainty": "not compared with the database page (none for this form)" + }, + "indexY": { + "model": "riiFormula4", + "coefficients": [2.539, 0.01277, 0, 0.01189, 1, 0, 0, 0, 1, -0.01849, 2, 4.3025e-05, 4, -2.9131e-05, 6], + "validRangeUm": [0.19, 2.6], + "citation": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "status": "unverified", + "uncertainty": "not compared with the database page (none for this form)" + }, + "indexZ": { + "model": "riiFormula4", + "coefficients": [2.5865, 0.0131, 0, 0.01223, 1, 0, 0, 0, 1, -0.01862, 2, 4.5778e-05, 4, -3.2526e-05, 6], + "validRangeUm": [0.19, 2.6], + "citation": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "status": "unverified", + "uncertainty": "not compared with the database page (none for this form)" + }, + "d": { + "pointGroup": "mm2 (polar axis = Y)", + "tensor": "mm2:y", + "dPmPerV": { + "d31": -0.67, + "d32": 0.85, + "d33": 0.04 + }, + "source": "Dmitriev, Gurzadyan & Nikogosyan, Handbook of Nonlinear Optical Crystals, as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 1.9e-20, + "source": "approximate value of the original code (no source recorded)", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.19, 2.6], + "source": "Kato, IEEE J. Quantum Electron. 30, 2950 (1994)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/LiNbO3.json b/src/UltrafastEngine.Core/Data/materials/crystals/LiNbO3.json new file mode 100644 index 0000000..025871b --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/LiNbO3.json @@ -0,0 +1,76 @@ +{ + "file": "LiNbO3", + "id": "LiNbO3", + "kind": "crystal", + "listOrder": 5, + "aliases": [ + "ln", + "cln", + "ppln" + ], + "aliasDisplayNames": { + "ppln": "PPLN (congruent)" + }, + "displayName": "LiNbO3 (congruent)", + "opticalClass": "uniaxial", + "temperatureDependent": true, + "references": "e: Jundt (1997) (temperature dependent); o: Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (21 C); d33 = 25, d31 = 4.6, d22 = 2.1 pm/V (Shoji et al., JOSA B 14, 2268 (1997)); n2: DeSalvo et al. (1996)", + "description": "congruent lithium niobate, negative uniaxial; the aliases ppln, cln name the poled / congruent forms", + "quantities": { + "indexX": { + "model": "riiFormula2", + "coefficients": [0, 2.6734, 0.01764, 1.229, 0.05914, 12.614, 474.6], + "validRangeUm": [0.4, 5.0], + "citation": "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (refractiveindex.info LiNbO3/Zelmon-o)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page LiNbO3_Zelmon-o (formula 2, 7 coefficients)" + }, + "indexZ": { + "model": "jundt", + "a": [5.35583, 0.100473, 0.20692, 100.0, 11.34927, 0.015334], + "b": [4.629e-07, 3.862e-08, -8.9e-09, 2.657e-05], + "citation": "Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray", + "validRangeUm": [0.4, 5.0], + "source": "Jundt, Opt. Lett. 22, 1553 (1997), congruent LiNbO3 e-ray (temperature dependent, T in degC)", + "status": "unverified", + "uncertainty": "not compared with the paper", + "conditions": { + "temperatureK": 297.65 + } + }, + "d": { + "pointGroup": "3m", + "tensor": "3m", + "dPmPerV": { + "d22": 2.1, + "d31": 4.6, + "d33": 25.0 + }, + "source": "Shoji et al., JOSA B 14, 2268 (1997), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 9.3e-20, + "source": "DeSalvo et al., IEEE JQE 32, 1324 (1996), as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.4, 5.0], + "source": "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/crystals/MgOLiNbO3.json b/src/UltrafastEngine.Core/Data/materials/crystals/MgOLiNbO3.json new file mode 100644 index 0000000..7550c51 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/crystals/MgOLiNbO3.json @@ -0,0 +1,76 @@ +{ + "file": "MgOLiNbO3", + "id": "MgO:LiNbO3", + "kind": "crystal", + "listOrder": 6, + "aliases": [ + "mgln", + "mgo:ln", + "mgo:ppln", + "ppmgln", + "mgo:cln" + ], + "displayName": "MgO:LiNbO3 (5 mol%)", + "opticalClass": "uniaxial", + "temperatureDependent": true, + "notes": "MgO:LiNbO3: the o-ray index is that of congruent LiNbO3 (only e-ray data for the doped crystal are built in); type-0 (eee) QPM is unaffected", + "references": "e: Gayer et al. (2008) (temperature dependent; the room-temperature coefficients are verified, the temperature coefficient b4 = 1.516e-4 could not be cross-checked -- pyNLO uses 1.526e-5); o: congruent LiNbO3, Zelmon, Small & Jundt, JOSA B 14, 3319 (1997); d33 = 25 pm/V", + "description": "5 mol% MgO-doped congruent lithium niobate", + "quantities": { + "indexX": { + "model": "riiFormula2", + "coefficients": [0, 2.6734, 0.01764, 1.229, 0.05914, 12.614, 474.6], + "validRangeUm": [0.4, 5.0], + "citation": "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997)", + "conditions": { + "temperatureK": 293.15 + }, + "source": "Zelmon, Small & Jundt, JOSA B 14, 3319 (1997) (refractiveindex.info LiNbO3/Zelmon-o)", + "status": "verified", + "verification": "agrees with the coefficients of the fetched page LiNbO3_Zelmon-o (formula 2, 7 coefficients)" + }, + "indexZ": { + "model": "jundt", + "a": [5.756, 0.0983, 0.202, 189.32, 12.52, 0.0132], + "b": [2.86e-06, 4.7e-08, 6.113e-08, 0.0001516], + "citation": "Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray", + "validRangeUm": [0.5, 4.0], + "source": "Gayer et al., Appl. Phys. B 91, 343 (2008), 5% MgO:CLN e-ray (temperature dependent, T in degC)", + "status": "unverified", + "uncertainty": "the temperature coefficient b4 = 1.516e-4 could not be cross-checked (pyNLO uses 1.526e-5)", + "conditions": { + "temperatureK": 297.65 + } + }, + "d": { + "pointGroup": "3m", + "tensor": "3m", + "dPmPerV": { + "d22": 2.1, + "d31": 4.4, + "d33": 25.0 + }, + "source": "d33 = 25 pm/V as cited by the original code; not re-read", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "wavelengthM": 1.064e-06 + } + }, + "n2": { + "valueM2PerW": 9.3e-20, + "source": "copy of the congruent LiNbO3 value (DeSalvo et al. 1996); not re-read", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "validity": { + "validRangeUm": [0.5, 4.0], + "source": "Gayer et al., Appl. Phys. B 91, 343 (2008)", + "status": "approximate", + "uncertainty": "range of the index data; outside it the formula is extrapolated" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/defaults/GainMediumDefaults.json b/src/UltrafastEngine.Core/Data/materials/defaults/GainMediumDefaults.json new file mode 100644 index 0000000..6f230cb --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/defaults/GainMediumDefaults.json @@ -0,0 +1,16 @@ +{ + "id": "defaults:gainMedium", + "kind": "default", + "listOrder": 1, + "description": "What the engine assumes for a user-defined gain medium that does not state its own thermal data. Used for the thermal lens only.", + "quantities": { + "thermal": { + "thermalConductivityWPerMK": 10, + "dnDTPerK": 7.8e-6, + "source": "generic YAG-like values chosen in the original code; not tied to a measurement", + "status": "unverified", + "conditions": {}, + "uncertainty": "a factor of 2 for other hosts (sapphire 33 W/(m K), glasses ~1 W/(m K); dn/dT changes sign between hosts)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/defaults/SolidIonizationDefaults.json b/src/UltrafastEngine.Core/Data/materials/defaults/SolidIonizationDefaults.json new file mode 100644 index 0000000..3ac3f9f --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/defaults/SolidIonizationDefaults.json @@ -0,0 +1,18 @@ +{ + "id": "defaults:solidIonization", + "kind": "default", + "listOrder": 2, + "description": "What the engine assumes for a solid without ionisation data when a run gives only some of the inputs (the band gap is always required).", + "quantities": { + "ionization": { + "effectiveMassRatio": 0.635, + "neutralDensityM3": 2.1e28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical Keldysh / plasma inputs of fused silica, used as the generic dielectric (original code; no source recorded)", + "status": "unverified", + "conditions": {}, + "uncertainty": "sample-dependent by tens of percent, and different for other dielectrics" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gain/NdYAG.json b/src/UltrafastEngine.Core/Data/materials/gain/NdYAG.json new file mode 100644 index 0000000..843c133 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gain/NdYAG.json @@ -0,0 +1,73 @@ +{ + "file": "NdYAG", + "id": "Nd:YAG", + "kind": "gain", + "listOrder": 3, + "aliases": [ + "ndyag" + ], + "description": "Neodymium-doped YAG (four-level)", + "quantities": { + "doping": { + "host": "YAG", + "dopingDensityM3": 1.38e+26, + "source": "1 at.% Nd (Koechner), as in the original code", + "status": "approximate", + "uncertainty": "user-specific: set dopingDensityM3" + }, + "lifetime": { + "lifetimeS": 0.00023, + "source": "Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us), as cited by the original code; not re-read", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": {} + }, + "thermal": { + "thermalConductivityWPerMK": 10.5, + "dnDTPerK": 7.8e-06, + "source": "typical values of Nd:YAG (original code)", + "status": "unverified", + "conditions": {}, + "uncertainty": "not stated in the original code" + }, + "stress": { + "thermalExpansionPerK": 7.5e-06, + "stressCr": 0.017, + "stressCphi": -0.0025, + "source": "photo-elastic constants of YAG after Koechner (as for Yb:YAG); not re-read", + "status": "unverified", + "conditions": {}, + "uncertainty": "not stated in the original code" + }, + "emission": { + "shape": "gaussians", + "bands": [ + { + "wavelengthM": 1.0642e-06, + "sigmaCm2": 2.8e-19, + "fwhmM": 6e-10 + } + ], + "description": "Gaussian fit: 2.8e-19 cm^2 at 1064.2 nm, 0.6 nm FWHM", + "source": "Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us); four-level; approximate analytic fit", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": {} + }, + "absorption": { + "shape": "gaussians", + "bands": [ + { + "wavelengthM": 8.085e-07, + "sigmaCm2": 7e-20, + "fwhmM": 2.5e-09 + } + ], + "description": "Gaussian fit: 7e-20 cm^2 at 808.5 nm, 2.5 nm FWHM", + "source": "Koechner, Solid-State Laser Engineering; approximate analytic fit", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": {} + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gain/TiSapphire.json b/src/UltrafastEngine.Core/Data/materials/gain/TiSapphire.json new file mode 100644 index 0000000..79ac529 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gain/TiSapphire.json @@ -0,0 +1,65 @@ +{ + "file": "TiSapphire", + "id": "Ti:sapphire", + "kind": "gain", + "listOrder": 2, + "aliases": [ + "tisa", + "tisapphire", + "ti:sa", + "ti:al2o3" + ], + "description": "Titanium-doped sapphire (four-level)", + "notes": "Ti:sapphire is treated as a four-level system (no lasing-band absorption); the residual infrared absorption (figure of merit) is not included -- add it with absorptionProfile or a material attenuation", + "quantities": { + "doping": { + "host": "Sapphire", + "dopingDensityM3": 3.3e+25, + "source": "doping density of the original code; give your crystal's doping", + "status": "approximate", + "uncertainty": "user-specific: set dopingDensityM3" + }, + "lifetime": { + "lifetimeS": 3.2e-06, + "source": "Moulton, JOSA B 3, 125 (1986) (lifetime 3.2 us), as cited by the original code; not re-read", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": {} + }, + "thermal": { + "thermalConductivityWPerMK": 33, + "dnDTPerK": 1.3e-05, + "source": "typical values of sapphire (original code)", + "status": "unverified", + "conditions": {}, + "uncertainty": "not stated in the original code" + }, + "emission": { + "shape": "asymmetricGaussian", + "peakWavelengthM": 7.95e-07, + "sigmaCm2": 3e-19, + "hwhmShortM": 7.5e-08, + "hwhmLongM": 1.4e-07, + "description": "asymmetric-Gaussian fit, pi polarisation: 3.0e-19 cm^2 at 795 nm, half widths 75 / 140 nm", + "source": "after Moulton, JOSA B 3, 125 (1986) (pi-polarised cross sections, lifetime 3.2 us); four-level; approximate analytic fit (~10-20 %) -- override with measured tables", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": {} + }, + "absorption": { + "shape": "gaussians", + "bands": [ + { + "wavelengthM": 4.9e-07, + "sigmaCm2": 6.4e-20, + "fwhmM": 1.15e-07 + } + ], + "description": "Gaussian fit, pi polarisation: 6.4e-20 cm^2 at 490 nm, 115 nm FWHM (~4.4e-20 cm^2 at 532 nm)", + "source": "after Moulton, JOSA B 3, 125 (1986) (pi-polarised); approximate analytic fit (~10-20 %) -- override with measured tables", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": {} + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gain/YbYAG.json b/src/UltrafastEngine.Core/Data/materials/gain/YbYAG.json new file mode 100644 index 0000000..37c578f --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gain/YbYAG.json @@ -0,0 +1,105 @@ +{ + "file": "YbYAG", + "id": "Yb:YAG", + "kind": "gain", + "listOrder": 1, + "aliases": [ + "ybyag" + ], + "description": "Ytterbium-doped YAG (quasi-three-level)", + "notes": "Yb:YAG cross sections are an analytic room-temperature fit; the McCumber relation gives reabsorption at the lasing wavelength (~0.1e-20 cm^2 at 1030 nm). Cryogenic linewidths are not modelled (only the Boltzmann factor follows temperatureK)", + "quantities": { + "doping": { + "host": "YAG", + "dopingDensityM3": 2.76e+26, + "source": "doping density of the original code (2 x 1.38e26 m^-3); give your crystal's doping", + "status": "approximate", + "uncertainty": "user-specific: set dopingDensityM3 or dopingAtPercent" + }, + "lifetime": { + "lifetimeS": 0.00095, + "source": "Sumida and Fan, OSA TOPS 1995 (upper-state lifetime 0.95 ms), as cited by the original code; not re-read", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": {} + }, + "zeroLine": { + "zeroLineWavelengthM": 9.688e-07, + "source": "zero-phonon line 968.8 nm of Yb:YAG, as used by the original code for the McCumber relation; not re-read", + "status": "approximate", + "relativeUncertainty": 0.001, + "conditions": {} + }, + "thermal": { + "thermalConductivityWPerMK": 9.5, + "dnDTPerK": 7.8e-06, + "source": "typical values of Yb:YAG (original code); conductivity falls with doping", + "status": "unverified", + "conditions": {}, + "uncertainty": "not stated in the original code" + }, + "stress": { + "thermalExpansionPerK": 7.5e-06, + "stressCr": 0.017, + "stressCphi": -0.0025, + "source": "photo-elastic constants of YAG after Koechner, Solid-State Laser Engineering (thermal-effects chapter), as cited by the original code; not re-read", + "status": "unverified", + "conditions": {}, + "uncertainty": "not stated in the original code" + }, + "emission": { + "shape": "gaussians", + "bands": [ + { + "wavelengthM": 1.03e-06, + "sigmaCm2": 2.1e-20, + "fwhmM": 8.5e-09 + }, + { + "wavelengthM": 1.0485e-06, + "sigmaCm2": 3.2e-21, + "fwhmM": 1.6e-08 + }, + { + "wavelengthM": 1.015e-06, + "sigmaCm2": 3.5e-21, + "fwhmM": 3e-08 + } + ], + "description": "Gaussian-band fit: 2.1e-20 cm^2 at 1030 nm (8.5 nm FWHM), 1048 nm shoulder, broad base", + "source": "room-temperature values after Koerner et al., Appl. Phys. B 75, 2002 (cross sections) and Sumida & Fan, OSA TOPS 1995 (lifetime 0.95 ms); lasing-band absorption and pump-band emission from the McCumber relation (zero line 968.8 nm); approximate analytic fit (~10-20 %) -- override with measured tables", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "temperatureK": 293.15 + } + }, + "absorption": { + "shape": "gaussians", + "bands": [ + { + "wavelengthM": 9.405e-07, + "sigmaCm2": 7.7e-21, + "fwhmM": 1.8e-08 + }, + { + "wavelengthM": 9.688e-07, + "sigmaCm2": 8.5e-21, + "fwhmM": 2.8e-09 + }, + { + "wavelengthM": 9.15e-07, + "sigmaCm2": 2.5e-21, + "fwhmM": 1.5e-08 + } + ], + "description": "Gaussian-band fit: 0.77e-20 cm^2 at 940 nm (18 nm FWHM), 0.85e-20 cm^2 at 969 nm (2.8 nm)", + "source": "pump-band absorption of Yb:YAG, approximate analytic fit (~10-20 %), after the same references as the emission band -- override with measured tables", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/Air.json b/src/UltrafastEngine.Core/Data/materials/gases/Air.json new file mode 100644 index 0000000..aa37cc3 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/Air.json @@ -0,0 +1,116 @@ +{ + "id": "Air", + "kind": "gas", + "listOrder": 1, + "aliases": [ + "AIR", + "" + ], + "description": "Dry air with 450 ppm CO2", + "quantities": { + "index": { + "model": "ciddor", + "temperatureC": 20.0, + "pressurePa": 101325.0, + "relativeHumidityPercent": 0.0, + "co2Ppm": 450.0, + "borzsonyiCoefficients": [0.0001492644, 1.936e-05, 0.0004180757, 0.007434], + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (constants in the code); bundled check page other/air/Ciddor", + "status": "verified", + "conditions": { + "temperatureK": 293.15, + "pressureBar": 1.01325 + } + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 8.5e-24, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked; air as a mixture of N2 and O2", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "composition": { + "fractions": { + "N2": 0.7808, + "O2": 0.2095, + "Ar": 0.0093 + }, + "source": "mole fractions of dry air (standard atmosphere)", + "status": "verified", + "conditions": {} + }, + "standardAirDispersion": { + "k0PerUm2": 238.0185, + "k1PerUm2": 5792105, + "k2PerUm2": 57.362, + "k3PerUm2": 167917, + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996), dry-air dispersion: (n-1) 1e8 = k1 / (k0 - s^2) + k3 / (k2 - s^2), s = 1/lambda in 1/um; refractiveindex.info other/air/Ciddor (standard air 15 C, 101.325 kPa, 450 ppm CO2)", + "status": "verified", + "verification": "the page's coefficients 0.05792105, 238.0185, 0.00167917, 57.362 equal k1 / 1e8, k0, k3 / 1e8, k2; the engine at 15 C, 101325 Pa, dry, 450 ppm equals the page's formula (test CatalogueTests.CiddorAir_AtStandardConditions_EqualsTheDatabaseFormula)", + "conditions": { + "temperatureK": 288.15, + "pressureBar": 1.01325 + } + }, + "co2Dependence": { + "co2SlopeFractionPerPpm": 5.34e-07, + "co2ReferencePpm": 450.0, + "airMolarMassGPerMol": 28.9635, + "airMolarMassSlopeGPerMolPerPpm": 1.2011e-05, + "airMolarMassReferencePpm": 400.0, + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (dispersion changes by 0.534e-6 per ppm of CO2 about 450 ppm; molar mass of dry air 28.9635 + 12.011e-6 (x_CO2 - 400) g/mol); not re-read", + "status": "unverified", + "uncertainty": "not compared with the paper; the dry-air index at 633 nm is checked against the paper's value in FoundationTests" + }, + "waterVaporDispersion": { + "polynomialCoefficients": [295.235, 2.6422, -0.03238, 0.004028], + "units": "(n_ws - 1) 1e8 = cf (w0 + w1 s^2 + w2 s^4 + w3 s^6), s in 1/um", + "correctionFactor": 1.022, + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996) water-vapour dispersion (w0..w3, cf); not re-read", + "status": "unverified", + "uncertainty": "humid-air results are not compared with the paper" + }, + "thermodynamics": { + "gasConstantJPerMolK": 8.31451, + "waterMolarMassKgPerMol": 0.018015, + "airMolarMassKgPerGPerMolFactor": 0.001, + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996) (R = 8.314510 J/(mol K), M_w = 0.018015 kg/mol); not re-read", + "status": "unverified", + "uncertainty": "constants of the paper; transcription not re-read" + }, + "saturationVapourPressure": { + "coefficients": [1.2378847e-05, -0.019121316, 33.93711047, -6343.1645], + "units": "p_sv = exp(A T^2 + B T + C + D / T) in Pa, T in K", + "source": "CIPM-2007 / Ciddor 1996 saturation vapour pressure of water; transcription not re-read", + "status": "unverified", + "uncertainty": "not compared with the standard" + }, + "enhancementFactor": { + "coefficients": [1.00062, 3.14e-08, 5.6e-07], + "units": "f = a + b p + c t^2, p in Pa, t in degC", + "source": "enhancement factor of water vapour in air (Ciddor 1996 / CIPM-2007); transcription not re-read", + "status": "unverified", + "uncertainty": "not compared with the standard" + }, + "compressibility": { + "coefficients": [1.58123e-06, -2.9331e-08, 1.1043e-10, 5.707e-06, -2.051e-08, 0.00019898, -2.376e-06, 1.83e-11, -7.65e-09], + "units": "a0, a1, a2, b0, b1, c0, c1, d, e of Z(p, T, x_w)", + "source": "compressibility factor of moist air (Ciddor 1996 / CIPM-2007); transcription not re-read", + "status": "unverified", + "uncertainty": "not compared with the standard" + }, + "referenceStates": { + "standardAirPressurePa": 101325.0, + "standardAirTemperatureK": 288.15, + "waterVaporPressurePa": 1333.0, + "waterVaporTemperatureK": 293.15, + "source": "P. E. Ciddor, Appl. Opt. 35, 1566 (1996) reference states of the density ratios", + "status": "approximate", + "uncertainty": "definitions; transcription not re-read" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/Ar.json b/src/UltrafastEngine.Core/Data/materials/gases/Ar.json new file mode 100644 index 0000000..37a5449 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/Ar.json @@ -0,0 +1,43 @@ +{ + "id": "Ar", + "kind": "gas", + "listOrder": 12, + "aliases": [ + "ARGON" + ], + "description": "Argon", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [0.0002033229, 0.00020612, 0.0003445831, 0.008066], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Ar/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/Ar/Borzsonyi: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 1e-23, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 15.76, + "orbitalL": 1, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/CO2.json b/src/UltrafastEngine.Core/Data/materials/gases/CO2.json new file mode 100644 index 0000000..b0ae0a4 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/CO2.json @@ -0,0 +1,68 @@ +{ + "id": "CO2", + "kind": "gas", + "listOrder": 17, + "aliases": [ + "CARBONDIOXIDE" + ], + "description": "Carbon dioxide", + "quantities": { + "index": { + "model": "dispersionFormula", + "c0": 0.0, + "b": [0.06991, 0.0014472, 6.42941e-05, 5.21306e-05, 1.46847e-06], + "cPerUm2": [166.175, 79.609, 56.3064, 46.0196, 0.0584738], + "referenceTemperatureK": 273.15, + "conditions": { + "temperatureK": 273.15, + "pressureBar": 1.01325 + }, + "source": "Bideau-Mehu et al., Opt. Commun. 9, 432 (1973), 273.15 K (refractiveindex.info main/CO2/Bideau-Mehu)", + "status": "verified", + "verification": "agrees with main/CO2/Bideau-Mehu.yml: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 1.4e-23, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 13.777, + "orbitalL": 1, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + }, + "rotation": { + "rotationalConstantBPerCm": 0.39021, + "centrifugalDistortionDPerCm": 1.33e-07, + "polarizabilityAnisotropyA3": 2.1, + "nuclearSpinWeightEven": 1, + "nuclearSpinWeightOdd": 0, + "source": "rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry", + "status": "approximate", + "relativeUncertainty": 0.15, + "conditions": { + "temperatureK": 293.15 + } + }, + "dephasing": { + "t2At1BarS": 5e-11, + "source": "rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it", + "status": "unverified", + "relativeUncertainty": 0.5, + "conditions": { + "pressureBar": 1.0, + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/H2.json b/src/UltrafastEngine.Core/Data/materials/gases/H2.json new file mode 100644 index 0000000..bf59d8a --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/H2.json @@ -0,0 +1,68 @@ +{ + "id": "H2", + "kind": "gas", + "listOrder": 18, + "aliases": [ + "HYDROGEN" + ], + "description": "Hydrogen", + "quantities": { + "index": { + "model": "dispersionFormula", + "c0": 0.0, + "b": [0.0148956, 0.0049037], + "cPerUm2": [180.7, 92.0], + "referenceTemperatureK": 273.15, + "conditions": { + "temperatureK": 273.15, + "pressureBar": 1.01325 + }, + "source": "Peck and Huang, J. Opt. Soc. Am. 67, 1550 (1977), 273.15 K (refractiveindex.info main/H2/Peck)", + "status": "verified", + "verification": "agrees with main/H2/Peck.yml: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 6.9e-24, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 15.426, + "orbitalL": 0, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + }, + "rotation": { + "rotationalConstantBPerCm": 59.322, + "centrifugalDistortionDPerCm": 0.0471, + "polarizabilityAnisotropyA3": 0.3, + "nuclearSpinWeightEven": 1, + "nuclearSpinWeightOdd": 3, + "source": "rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry", + "status": "approximate", + "relativeUncertainty": 0.15, + "conditions": { + "temperatureK": 293.15 + } + }, + "dephasing": { + "t2At1BarS": 3e-10, + "source": "rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it", + "status": "unverified", + "relativeUncertainty": 0.5, + "conditions": { + "pressureBar": 1.0, + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/He.json b/src/UltrafastEngine.Core/Data/materials/gases/He.json new file mode 100644 index 0000000..18527aa --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/He.json @@ -0,0 +1,43 @@ +{ + "id": "He", + "kind": "gas", + "listOrder": 10, + "aliases": [ + "HELIUM" + ], + "description": "Helium", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [4.97777e-05, 2.854e-05, 1.85694e-05, 0.00776], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/He/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/He/Borzsonyi: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 3.5e-25, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 24.587, + "orbitalL": 0, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/Kr.json b/src/UltrafastEngine.Core/Data/materials/gases/Kr.json new file mode 100644 index 0000000..02993dd --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/Kr.json @@ -0,0 +1,43 @@ +{ + "id": "Kr", + "kind": "gas", + "listOrder": 13, + "aliases": [ + "KRYPTON" + ], + "description": "Krypton", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [0.0002610288, 2.01e-06, 0.0005694682, 0.010043], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Kr/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/Kr/Borzsonyi: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 2.8e-23, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 13.999, + "orbitalL": 1, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/N2.json b/src/UltrafastEngine.Core/Data/materials/gases/N2.json new file mode 100644 index 0000000..fdb3823 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/N2.json @@ -0,0 +1,66 @@ +{ + "id": "N2", + "kind": "gas", + "listOrder": 15, + "aliases": [ + "NITROGEN" + ], + "description": "Nitrogen", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [0.0003920995, 0.00114624, 0.0001880648, 0.013476], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/N2/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/N2/Borzsonyi: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 8e-24, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 15.581, + "orbitalL": 0, + "zEff": 0.9, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + }, + "rotation": { + "rotationalConstantBPerCm": 1.98957, + "centrifugalDistortionDPerCm": 5.76e-06, + "polarizabilityAnisotropyA3": 0.7, + "nuclearSpinWeightEven": 6, + "nuclearSpinWeightOdd": 3, + "source": "rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry", + "status": "approximate", + "relativeUncertainty": 0.15, + "conditions": { + "temperatureK": 293.15 + } + }, + "dephasing": { + "t2At1BarS": 1e-10, + "source": "rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it", + "status": "unverified", + "relativeUncertainty": 0.5, + "conditions": { + "pressureBar": 1.0, + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/Ne.json b/src/UltrafastEngine.Core/Data/materials/gases/Ne.json new file mode 100644 index 0000000..fad9a62 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/Ne.json @@ -0,0 +1,43 @@ +{ + "id": "Ne", + "kind": "gas", + "listOrder": 11, + "aliases": [ + "NEON" + ], + "description": "Neon", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [9.15448e-05, 0.00065697, 4.01863e-05, 0.005728], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Ne/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/Ne/Borzsonyi: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 7.5e-25, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 21.565, + "orbitalL": 1, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/O2.json b/src/UltrafastEngine.Core/Data/materials/gases/O2.json new file mode 100644 index 0000000..cc5bbcf --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/O2.json @@ -0,0 +1,68 @@ +{ + "id": "O2", + "kind": "gas", + "listOrder": 16, + "aliases": [ + "OXYGEN" + ], + "description": "Oxygen", + "quantities": { + "index": { + "model": "dispersionFormula", + "c0": 0.0001181494, + "b": [0.009708931], + "cPerUm2": [75.4], + "referenceTemperatureK": 293.15, + "conditions": { + "temperatureK": 293.15, + "pressureBar": 1.01325 + }, + "source": "Zhang et al., Appl. Opt. 47, 3143 (2008), 293.15 K (refractiveindex.info main/O2/Zhang)", + "status": "verified", + "verification": "agrees with main/O2/Zhang.yml: max |dn| = 0.0e+00" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 1.03e-23, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 12.07, + "orbitalL": 1, + "zEff": 0.53, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices; the effective charge of the molecule is an empirical choice", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + }, + "rotation": { + "rotationalConstantBPerCm": 1.43768, + "centrifugalDistortionDPerCm": 4.85e-06, + "polarizabilityAnisotropyA3": 1.14, + "nuclearSpinWeightEven": 0, + "nuclearSpinWeightOdd": 1, + "source": "rotational constants: NIST / Herzberg (as cited by the original code); polarisability anisotropy: Bridge and Buckingham, Proc. R. Soc. A 295, 334 (1966), as cited by the original code; nuclear-spin weights from the molecular symmetry", + "status": "approximate", + "relativeUncertainty": 0.15, + "conditions": { + "temperatureK": 293.15 + } + }, + "dephasing": { + "t2At1BarS": 8e-11, + "source": "rotational coherence time at 1 bar and 293 K, scaled as 1/p and sqrt(T); typical value, the original code did not record a source; the 2011 fs/ps rotational CARS measurements of self-broadened N2 and O2 (1-20 atm) are the place to check it", + "status": "unverified", + "relativeUncertainty": 0.5, + "conditions": { + "pressureBar": 1.0, + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/gases/Xe.json b/src/UltrafastEngine.Core/Data/materials/gases/Xe.json new file mode 100644 index 0000000..8100476 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/gases/Xe.json @@ -0,0 +1,43 @@ +{ + "id": "Xe", + "kind": "gas", + "listOrder": 14, + "aliases": [ + "XENON" + ], + "description": "Xenon", + "quantities": { + "index": { + "model": "borzsonyi", + "coefficients": [0.0010370161, 0.01275, 0.0003122861, 0.000561], + "validRangeUm": [0.4, 1.0], + "conditions": { + "temperatureK": 273.0, + "pressureBar": 1.01325 + }, + "source": "A. Borzsonyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovacs, K. Osvay, Appl. Opt. 47, 4856 (2008) (refractiveindex.info main/Xe/Borzsonyi), 273 K and 1 bar", + "status": "verified", + "verification": "agrees vs refractiveindex.info main/Xe/Borzsonyi: max |dn| = 0.0e+00 (the page fixes a typo of the paper in Xe: 12.75e-3)" + }, + "n2Electronic": { + "valueM2PerWPerBar273K": 8e-23, + "source": "electronic (instantaneous) n2 at 1 bar and 273 K near 800 nm; the original code cites the compilation of Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012; values differ by ~20 % between sources; not re-checked", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "wavelengthM": 8e-07, + "pressureBar": 1.0, + "temperatureK": 273.0 + } + }, + "ionization": { + "ionizationEv": 12.13, + "orbitalL": 1, + "zEff": 1.0, + "source": "ionisation potential: NIST Atomic Spectra Database (as cited by the original code); orbital l and zEff: standard ADK / PPT choices", + "status": "approximate", + "conditions": {}, + "uncertainty": "ionisation potentials are tabulated constants; the effective charge and orbital l are model choices" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/raman/blowWood.json b/src/UltrafastEngine.Core/Data/materials/raman/blowWood.json new file mode 100644 index 0000000..2718382 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/raman/blowWood.json @@ -0,0 +1,39 @@ +{ + "id": "blowWood", + "kind": "raman", + "aliases": [ + "silica" + ], + "description": "Fused silica, single damped oscillator", + "quantities": { + "response": { + "model": "dampedOscillator", + "tau1S": 1.22e-14, + "tau2S": 3.2e-14, + "source": "K. J. Blow and D. Wood, IEEE J. Quantum Electron. 25, 2665 (1989); the constants 12.2 fs and 32 fs are the ones in common use for fused silica", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": { + "host": "fused silica" + } + }, + "ramanFraction": { + "value": 0.18, + "source": "Blow-Wood / Agrawal, Nonlinear Fiber Optics: fR = 0.18 for fused silica; Lin and Agrawal (2006) give 0.245 with the boson peak", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "host": "fused silica" + } + }, + "anisotropicFraction": { + "value": 0.25, + "source": "anisotropic share of the Raman response (Lin and Agrawal fb + fc = 0.25), from the measured depolarisation of the Raman spectrum", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "host": "fused silica" + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/raman/hollenbeck.json b/src/UltrafastEngine.Core/Data/materials/raman/hollenbeck.json new file mode 100644 index 0000000..861059a --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/raman/hollenbeck.json @@ -0,0 +1,39 @@ +{ + "id": "hollenbeck", + "kind": "raman", + "aliases": [], + "description": "Fused silica, 13 vibrational modes", + "quantities": { + "response": { + "model": "hollenbeckCantrell", + "modePositionsPerCm": [56.25, 100.0, 231.25, 362.5, 463.0, 497.0, 611.5, 691.67, 793.67, 835.5, 930.0, 1080.0, 1215.0], + "peakIntensities": [1.0, 11.4, 36.67, 67.67, 74.0, 4.5, 6.8, 4.6, 4.2, 4.5, 2.7, 3.1, 3.0], + "gaussianFwhmPerCm": [52.1, 110.42, 175.0, 162.5, 135.33, 24.5, 41.5, 155.0, 59.5, 64.3, 150.0, 91.0, 160.0], + "lorentzianFwhmPerCm": [17.37, 38.81, 58.33, 54.17, 45.11, 8.17, 13.83, 51.67, 19.83, 21.43, 50.0, 30.33, 53.33], + "source": "D. Hollenbeck and C. D. Cantrell, J. Opt. Soc. Am. B 19, 2886 (2002), Table 1 (intermediate-broadening model), as transcribed in the original code; not re-read", + "status": "unverified", + "conditions": { + "host": "fused silica" + }, + "uncertainty": "transcribed table of 13 modes, not re-read against the paper; the response is checked for the 13.2 THz gain peak" + }, + "ramanFraction": { + "value": 0.18, + "source": "same as the Blow-Wood model", + "status": "approximate", + "relativeUncertainty": 0.2, + "conditions": { + "host": "fused silica" + } + }, + "anisotropicFraction": { + "value": 0.25, + "source": "same as the Blow-Wood model", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "host": "fused silica" + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/raman/linAgrawal.json b/src/UltrafastEngine.Core/Data/materials/raman/linAgrawal.json new file mode 100644 index 0000000..abc74db --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/raman/linAgrawal.json @@ -0,0 +1,32 @@ +{ + "id": "linAgrawal", + "kind": "raman", + "aliases": [], + "description": "Fused silica with boson peak", + "quantities": { + "response": { + "model": "linAgrawal", + "tau1S": 1.22e-14, + "tau2S": 3.2e-14, + "taubS": 9.6e-14, + "fa": 0.75, + "fb": 0.21, + "fc": 0.04, + "source": "Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006); pyNLO's 'full' model uses the same numbers", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": { + "host": "fused silica" + } + }, + "ramanFraction": { + "value": 0.245, + "source": "Q. Lin and G. P. Agrawal, Opt. Lett. 31, 3086 (2006)", + "status": "approximate", + "relativeUncertainty": 0.1, + "conditions": { + "host": "fused silica" + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/raman/orientational.json b/src/UltrafastEngine.Core/Data/materials/raman/orientational.json new file mode 100644 index 0000000..eb95642 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/raman/orientational.json @@ -0,0 +1,21 @@ +{ + "id": "orientational", + "kind": "raman", + "aliases": [], + "description": "Molecular reorientation of a liquid (CS2 values)", + "quantities": { + "response": { + "model": "orientational", + "riseS": 2e-13, + "decayS": 1.6e-12, + "anisotropicFraction": 1.5, + "source": "h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps: typical CS2 values of the original code (decay ~1.6-2 ps is the commonly quoted reorientation time); the rise time and the exact decay are unverified; 1.5 = purely orientational (h_a = -h_b/3)", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "host": "CS2", + "temperatureK": 293.15 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Al2O3_film.json b/src/UltrafastEngine.Core/Data/materials/solids/Al2O3_film.json new file mode 100644 index 0000000..00b53a5 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Al2O3_film.json @@ -0,0 +1,26 @@ +{ + "id": "Al2O3_film", + "kind": "solid", + "listOrder": 44, + "aliases": [ + "AL2O3_FILM" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [1.62, 0.008, 0.0], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/BaF2.json b/src/UltrafastEngine.Core/Data/materials/solids/BaF2.json new file mode 100644 index 0000000..1ef4315 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/BaF2.json @@ -0,0 +1,44 @@ +{ + "id": "BaF2", + "kind": "solid", + "listOrder": 31, + "aliases": [ + "BAF2" + ], + "description": "Barium fluoride, cubic (m3m)", + "notes": "cubic (m3m); give the orientation with kerrCutAxis / kerrAzimuthDeg on the element (default [001] cut, x along [100])", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Li 1980", + "a": 1.33973, + "b": [0.8107, 0.19652, 4.52469], + "resonanceWavelengthsUm": [0.10065, 29.87, 53.82], + "validRangeUm": [0.15, 15.0], + "conditions": { + "temperatureK": 293.15 + }, + "source": "H. H. Li, J. Phys. Chem. Ref. Data 9, 161 (1980), 293 K (refractiveindex.info main/BaF2/Li)", + "status": "verified", + "verification": "agrees with main/BaF2/Li.yml to 2.2e-16 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 2e-20, + "source": "typical literature value for light along [100]; reported values spread 1.8-2.9e-20 m^2/W; quoted from memory in the original code, not re-checked", + "status": "unverified", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 1e-06, + "orientation": "[100]" + } + }, + "kerrAnisotropy": { + "value": -1.2, + "source": "sigma = (chi1111 - 3 chi1122) / chi1111; quoted from memory of Minkovski et al., Opt. Lett. 29, 2237 (2004) and Jullien et al., J. Opt. Soc. Am. B 22, 2635 (2005); not retrieved", + "status": "unverified", + "uncertainty": "unknown; check the papers. The Z-scan anisotropy paper of the Sheik-Bahae / Van Stryland group (CREOL publication 1820; a scanned PDF that could not be read here) gives BaF2 n2 along [100], [010], [110] at 532 and 1064 nm and a parameter u in chi(theta) = chi [1 + 2u (sin^4 theta - sin^2 theta)]; how u relates to sigma is still to be written down", + "conditions": {} + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/CS2.json b/src/UltrafastEngine.Core/Data/materials/solids/CS2.json new file mode 100644 index 0000000..20e81c2 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/CS2.json @@ -0,0 +1,51 @@ +{ + "id": "CS2", + "kind": "solid", + "listOrder": 32, + "aliases": [ + "CARBONDISULFIDE", + "CARBON_DISULFIDE" + ], + "description": "Liquid carbon disulfide, 20 C", + "notes": "liquid CS2 at 20 C; index fit valid 0.5-1.6 um; n2 = 3.0e-18 m^2/W total (ps pulses) of which 10 % electronic and 90 % orientational, h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps; the split and the time constants are unverified typical values (spread of the literature: a factor 2-3): override n2 / ramanFraction / raman", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "vacuum", + "name": "Kedenburg et al., Opt. Mater. Express 2, 1588 (2012), 20 C", + "a": 1.0, + "b": [1.50387], + "poleUm2": [0.03049], + "validRangeUm": [0.5, 1.6], + "conditions": { + "temperatureK": 293.15 + }, + "source": "S. Kedenburg et al., Opt. Mater. Express 2, 1588 (2012) (refractiveindex.info main/CS2/Kedenburg); one-pole fit valid 0.5-1.6 um, extrapolated outside", + "status": "verified", + "verification": "agrees with main/CS2/Kedenburg.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3e-18, + "source": "total n2 for ps pulses (electronic + orientational); typical value, the literature spread is a factor 2-3; not re-checked", + "status": "unverified", + "relativeUncertainty": 1.0, + "conditions": { + "wavelengthM": 8e-07, + "pulseDurationS": 1e-12, + "temperatureK": 293.15 + } + }, + "raman": { + "ref": "orientational" + }, + "ramanFraction": { + "value": 0.9, + "source": "orientational (delayed) share of the total n2; the electronic part 3e-19 m^2/W is the fs value quoted by Liu et al., Opt. Lett. 38, 1838 (2013) citing Ganeev 2004; not re-checked", + "status": "unverified", + "relativeUncertainty": 0.1, + "conditions": { + "pulseDurationS": 1e-12 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/CaF2.json b/src/UltrafastEngine.Core/Data/materials/solids/CaF2.json new file mode 100644 index 0000000..bb10047 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/CaF2.json @@ -0,0 +1,46 @@ +{ + "id": "CaF2", + "kind": "solid", + "listOrder": 4, + "aliases": [ + "CAF2" + ], + "description": "Calcium fluoride", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Malitson 1963", + "a": 1.0, + "b": [0.5675888, 0.4710914, 3.8484723], + "resonanceWavelengthsUm": [0.050263605, 0.1003909, 34.64904], + "validRangeUm": [0.23, 9.7], + "conditions": { + "temperatureK": 293.15 + }, + "source": "I. H. Malitson, Appl. Opt. 2, 1103 (1963) (via refractiveindex.info main/CaF2/Malitson)", + "status": "verified", + "verification": "agrees with main/CaF2/Malitson.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 1.9e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 10.0, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2.4e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Calcite-e.json b/src/UltrafastEngine.Core/Data/materials/solids/Calcite-e.json new file mode 100644 index 0000000..82caec6 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Calcite-e.json @@ -0,0 +1,30 @@ +{ + "id": "Calcite-e", + "kind": "solid", + "listOrder": 25, + "aliases": [], + "description": "Calcite, extraordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Ghosh 1999, extraordinary", + "a": 1.35859695, + "b": [0.8242783, 0.14429128], + "poleUm2": [0.0106689543, 120.0], + "conditions": { + "temperatureK": 293.15 + }, + "source": "G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/CaCO3/Ghosh-e)", + "status": "verified", + "verification": "agrees with main/CaCO3/Ghosh-e.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Calcite.json b/src/UltrafastEngine.Core/Data/materials/solids/Calcite.json new file mode 100644 index 0000000..109aaef --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Calcite.json @@ -0,0 +1,33 @@ +{ + "id": "Calcite", + "kind": "solid", + "listOrder": 24, + "aliases": [ + "CALCITE-O", + "CACO3" + ], + "description": "Calcite, ordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Ghosh 1999, ordinary", + "a": 1.73358749, + "b": [0.96464345, 1.82831454], + "poleUm2": [0.0194325203, 120.0], + "conditions": { + "temperatureK": 293.15 + }, + "source": "G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/CaCO3/Ghosh-o)", + "status": "verified", + "verification": "agrees with main/CaCO3/Ghosh-o.yml to 2.2e-16 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/FusedSilica.json b/src/UltrafastEngine.Core/Data/materials/solids/FusedSilica.json new file mode 100644 index 0000000..262af0f --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/FusedSilica.json @@ -0,0 +1,54 @@ +{ + "id": "FusedSilica", + "kind": "solid", + "listOrder": 2, + "aliases": [ + "F_SILICA", + "SILICA", + "FUSED_SILICA", + "FS", + "SIO2", + "UVFS" + ], + "description": "Fused silica", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Malitson 1965", + "a": 1.0, + "b": [0.6961663, 0.4079426, 0.8974794], + "resonanceWavelengthsUm": [0.0684043, 0.1162414, 9.896161], + "validRangeUm": [0.21, 6.7], + "conditions": { + "temperatureK": 293.15 + }, + "source": "I. H. Malitson, J. Opt. Soc. Am. 55, 1205 (1965); refractiveindex.info main/SiO2/Malitson", + "status": "verified", + "verification": "agrees with main/SiO2/Malitson.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 2.6e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "raman": { + "ref": "blowWood" + }, + "ionization": { + "bandGapEv": 9.0, + "effectiveMassRatio": 0.635, + "neutralDensityM3": 2.1e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values for Keldysh / plasma models of fused silica; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/HfO2.json b/src/UltrafastEngine.Core/Data/materials/solids/HfO2.json new file mode 100644 index 0000000..2cb4e53 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/HfO2.json @@ -0,0 +1,26 @@ +{ + "id": "HfO2", + "kind": "solid", + "listOrder": 42, + "aliases": [ + "HFO2" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [1.95, 0.016, 0.0], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/MgF2-e.json b/src/UltrafastEngine.Core/Data/materials/solids/MgF2-e.json new file mode 100644 index 0000000..af6ee0b --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/MgF2-e.json @@ -0,0 +1,43 @@ +{ + "id": "MgF2-e", + "kind": "solid", + "listOrder": 22, + "aliases": [], + "description": "Magnesium fluoride, extraordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Dodge 1984, extraordinary", + "a": 1.0, + "b": [0.41344023, 0.50497499, 2.4904862], + "resonanceWavelengthsUm": [0.03684262, 0.09076162, 23.771995], + "conditions": { + "temperatureK": 293.15 + }, + "source": "M. J. Dodge, Appl. Opt. 23, 1980 (1984) (via refractiveindex.info main/MgF2/Dodge-e)", + "status": "verified", + "verification": "agrees with main/MgF2/Dodge-e.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 9e-21, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 10.8, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "copy of the MgF2 values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/MgF2.json b/src/UltrafastEngine.Core/Data/materials/solids/MgF2.json new file mode 100644 index 0000000..9e8f982 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/MgF2.json @@ -0,0 +1,46 @@ +{ + "id": "MgF2", + "kind": "solid", + "listOrder": 8, + "aliases": [ + "MGF2", + "MGF2-O" + ], + "description": "Magnesium fluoride, ordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Dodge 1984, ordinary", + "a": 1.0, + "b": [0.48755108, 0.39875031, 2.3120353], + "resonanceWavelengthsUm": [0.04338408, 0.09461442, 23.793604], + "conditions": { + "temperatureK": 293.15 + }, + "source": "M. J. Dodge, Appl. Opt. 23, 1980 (1984) (via refractiveindex.info main/MgF2/Dodge-o)", + "status": "verified", + "verification": "agrees with main/MgF2/Dodge-o.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 9e-21, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 10.8, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/N-BK7.json b/src/UltrafastEngine.Core/Data/materials/solids/N-BK7.json new file mode 100644 index 0000000..eb10801 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/N-BK7.json @@ -0,0 +1,47 @@ +{ + "id": "N-BK7", + "kind": "solid", + "listOrder": 3, + "aliases": [ + "BK7", + "NBK7" + ], + "description": "Schott N-BK7 borosilicate crown", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Schott", + "a": 1.0, + "b": [1.03961212, 0.231792344, 1.01046945], + "poleUm2": [0.00600069867, 0.0200179144, 103.560653], + "validRangeUm": [0.3, 2.5], + "conditions": { + "temperatureK": 293.15 + }, + "source": "Schott N-BK7 data sheet (as listed on refractiveindex.info glass/schott/N-BK7)", + "status": "verified", + "verification": "agrees with glass/schott/N-BK7.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3.4e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 4.2, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/N-SF11.json b/src/UltrafastEngine.Core/Data/materials/solids/N-SF11.json new file mode 100644 index 0000000..36aed6d --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/N-SF11.json @@ -0,0 +1,35 @@ +{ + "id": "N-SF11", + "kind": "solid", + "listOrder": 7, + "aliases": [ + "SF11", + "NSF11" + ], + "description": "Schott N-SF11 dense flint", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Schott", + "a": 1.0, + "b": [1.73759695, 0.313747346, 1.89878101], + "poleUm2": [0.013188707, 0.0623068142, 155.23629], + "conditions": { + "temperatureK": 293.15 + }, + "source": "Schott N-SF11 data sheet (as listed on refractiveindex.info glass/schott/N-SF11)", + "status": "verified", + "verification": "agrees with glass/schott/N-SF11.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 1e-19, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Nb2O5.json b/src/UltrafastEngine.Core/Data/materials/solids/Nb2O5.json new file mode 100644 index 0000000..4ed8d15 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Nb2O5.json @@ -0,0 +1,26 @@ +{ + "id": "Nb2O5", + "kind": "solid", + "listOrder": 43, + "aliases": [ + "NB2O5" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [2.18, 0.04, 0.0], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Quartz-e.json b/src/UltrafastEngine.Core/Data/materials/solids/Quartz-e.json new file mode 100644 index 0000000..7e4fe90 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Quartz-e.json @@ -0,0 +1,43 @@ +{ + "id": "Quartz-e", + "kind": "solid", + "listOrder": 21, + "aliases": [], + "description": "Crystalline quartz, extraordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Ghosh 1999, crystalline quartz, extraordinary", + "a": 1.28851804, + "b": [1.09509924, 1.15662475], + "poleUm2": [0.0102101864, 100.0], + "conditions": { + "temperatureK": 293.15 + }, + "source": "G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/SiO2/Ghosh-e)", + "status": "verified", + "verification": "agrees with main/SiO2/Ghosh-e.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 9.0, + "effectiveMassRatio": 0.635, + "neutralDensityM3": 2.1e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "copy of the fused-silica values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Quartz.json b/src/UltrafastEngine.Core/Data/materials/solids/Quartz.json new file mode 100644 index 0000000..417c5fd --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Quartz.json @@ -0,0 +1,48 @@ +{ + "id": "Quartz", + "kind": "solid", + "listOrder": 20, + "aliases": [ + "QUARTZ", + "CRYSTALQUARTZ", + "SIO2_CRYSTAL", + "QUARTZ-O" + ], + "description": "Crystalline quartz, ordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Ghosh 1999, crystalline quartz, ordinary", + "a": 1.28604141, + "b": [1.07044083, 1.10202242], + "poleUm2": [0.0100585997, 100.0], + "conditions": { + "temperatureK": 293.15 + }, + "source": "G. Ghosh, Opt. Commun. 163, 95 (1999) (via refractiveindex.info main/SiO2/Ghosh-o)", + "status": "verified", + "verification": "agrees with main/SiO2/Ghosh-o.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 9.0, + "effectiveMassRatio": 0.635, + "neutralDensityM3": 2.1e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "copy of the fused-silica values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Sapphire-e.json b/src/UltrafastEngine.Core/Data/materials/solids/Sapphire-e.json new file mode 100644 index 0000000..5213384 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Sapphire-e.json @@ -0,0 +1,43 @@ +{ + "id": "Sapphire-e", + "kind": "solid", + "listOrder": 23, + "aliases": [], + "description": "Sapphire (Al2O3), extraordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Malitson & Dodge 1972, extraordinary", + "a": 1.0, + "b": [1.5039759, 0.55069141, 6.5927379], + "resonanceWavelengthsUm": [0.0740288, 0.1216529, 20.072248], + "conditions": { + "temperatureK": 293.15 + }, + "source": "I. H. Malitson and M. J. Dodge, J. Opt. Soc. Am. 62, 1405 (1972) (via refractiveindex.info main/Al2O3/Malitson-e)", + "status": "verified", + "verification": "agrees with main/Al2O3/Malitson-e.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 8.8, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2.3e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "copy of the sapphire values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Sapphire.json b/src/UltrafastEngine.Core/Data/materials/solids/Sapphire.json new file mode 100644 index 0000000..e9f174a --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Sapphire.json @@ -0,0 +1,47 @@ +{ + "id": "Sapphire", + "kind": "solid", + "listOrder": 5, + "aliases": [ + "SAPPHIRE", + "AL2O3", + "SAPPHIRE-O" + ], + "description": "Sapphire (Al2O3), ordinary ray", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Malitson & Dodge 1972, ordinary", + "a": 1.0, + "b": [1.4313493, 0.65054713, 5.3414021], + "resonanceWavelengthsUm": [0.0726631, 0.1193242, 18.028251], + "conditions": { + "temperatureK": 293.15 + }, + "source": "I. H. Malitson and M. J. Dodge, J. Opt. Soc. Am. 62, 1405 (1972) (via refractiveindex.info main/Al2O3/Malitson-o)", + "status": "verified", + "verification": "agrees with main/Al2O3/Malitson-o.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 3e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 8.8, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2.3e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/SiO2_film.json b/src/UltrafastEngine.Core/Data/materials/solids/SiO2_film.json new file mode 100644 index 0000000..eb6dd7b --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/SiO2_film.json @@ -0,0 +1,26 @@ +{ + "id": "SiO2_film", + "kind": "solid", + "listOrder": 45, + "aliases": [ + "SIO2_FILM" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [1.45, 0.0036, 0.0], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/TGG.json b/src/UltrafastEngine.Core/Data/materials/solids/TGG.json new file mode 100644 index 0000000..d2e8829 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/TGG.json @@ -0,0 +1,31 @@ +{ + "id": "TGG", + "kind": "solid", + "listOrder": 30, + "aliases": [], + "description": "Terbium gallium garnet (Faraday rotator)", + "notes": "dispersion fitted to the Schlarb & Sugg 1994 table (0.40-1.18 um, approximate); n2 not set (give one: the B-integral of an isolator matters)", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "TGG, single-pole fit to Schlarb & Sugg 1994", + "a": 1.24206176, + "b": [2.49181577], + "poleUm2": [0.01959882], + "validRangeUm": [0.40466, 1.17668], + "conditions": { + "temperatureK": 293.15 + }, + "source": "least-squares fit of n^2 = A + B l^2/(l^2 - C) to the nine measured points of U. Schlarb and B. Sugg, Phys. Status Solidi B 182, K91 (1994) (refractiveindex.info main/Tb3Ga5O12/Schlarb, 0.40466-1.17668 um, 20 C): largest deviation 6.6e-4", + "status": "approximate", + "uncertainty": "fit residual 6.6e-4 inside the measured range; extrapolation outside 0.40-1.18 um is not backed by data. The previous two-point fit (A = 1, B = 2.7769, C = 0.01656) was 0.011 away from the table at 1.08 um." + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Ta2O5.json b/src/UltrafastEngine.Core/Data/materials/solids/Ta2O5.json new file mode 100644 index 0000000..89d67de --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Ta2O5.json @@ -0,0 +1,26 @@ +{ + "id": "Ta2O5", + "kind": "solid", + "listOrder": 40, + "aliases": [ + "TA2O5" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [2.05, 0.02, 0.0012], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/TiO2.json b/src/UltrafastEngine.Core/Data/materials/solids/TiO2.json new file mode 100644 index 0000000..1ec48ea --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/TiO2.json @@ -0,0 +1,26 @@ +{ + "id": "TiO2", + "kind": "solid", + "listOrder": 41, + "aliases": [ + "TIO2" + ], + "description": "Ion-beam-sputtered thin film (Cauchy fit)", + "notes": "thin-film Cauchy approximation", + "quantities": { + "index": { + "model": "cauchy", + "coefficients": [2.2, 0.055, 0.003], + "source": "generic ion-beam-sputtered Cauchy fit n = A + B / l^2 + C / l^4 (l in um); the original code did not record a source; use your coating vendor's data for real designs", + "status": "approximate", + "uncertainty": "film indices depend on the process (a few 1e-2)", + "conditions": {} + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for thin films (give one if the B-integral of a coating matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/Vacuum.json b/src/UltrafastEngine.Core/Data/materials/solids/Vacuum.json new file mode 100644 index 0000000..580edc8 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/Vacuum.json @@ -0,0 +1,20 @@ +{ + "id": "Vacuum", + "kind": "solid", + "listOrder": 0, + "aliases": [], + "description": "Vacuum", + "quantities": { + "index": { + "model": "constant", + "value": 1.0, + "source": "definition", + "status": "verified" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "definition", + "status": "verified" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/YAG.json b/src/UltrafastEngine.Core/Data/materials/solids/YAG.json new file mode 100644 index 0000000..b96cbb2 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/YAG.json @@ -0,0 +1,43 @@ +{ + "id": "YAG", + "kind": "solid", + "listOrder": 6, + "aliases": [], + "description": "Yttrium aluminium garnet", + "quantities": { + "index": { + "model": "sellmeier", + "referenceMedium": "air", + "name": "Zelmon 1998", + "a": 1.0, + "b": [2.282, 3.27644], + "poleUm2": [0.01185, 282.734], + "conditions": { + "temperatureK": 293.15 + }, + "source": "D. E. Zelmon, D. L. Small, R. Page, Appl. Opt. 37, 4933 (1998) (via refractiveindex.info main/Y3Al5O12/Zelmon)", + "status": "verified", + "verification": "agrees with main/Y3Al5O12/Zelmon.yml to 0.0e+00 (n, 0.45-1.5 um)" + }, + "n2": { + "valueM2PerW": 6.2e-20, + "source": "typical literature value near 0.8-1 um (the literature spread is 10-30 %: wavelength, method, pulse duration); the original code did not record a source, not re-checked", + "status": "approximate", + "relativeUncertainty": 0.3, + "conditions": { + "wavelengthM": 8e-07 + } + }, + "ionization": { + "bandGapEv": 6.5, + "effectiveMassRatio": 0.5, + "neutralDensityM3": 2e+28, + "collisionTimeS": 1e-15, + "recombinationTimeS": 1.5e-13, + "source": "typical values; the original code did not record a source", + "status": "unverified", + "conditions": {}, + "uncertainty": "typical Keldysh-model inputs; sample-dependent by tens of percent (no source recorded)" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/YVO4-e.json b/src/UltrafastEngine.Core/Data/materials/solids/YVO4-e.json new file mode 100644 index 0000000..199da89 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/YVO4-e.json @@ -0,0 +1,31 @@ +{ + "id": "YVO4-e", + "kind": "solid", + "listOrder": 27, + "aliases": [], + "description": "Yttrium orthovanadate, extraordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "vendorSellmeier", + "referenceMedium": "air", + "name": "YVO4 extraordinary (common vendor Sellmeier)", + "a": 4.59905, + "numeratorUm2": 0.110534, + "poleUm2": 0.04813, + "irTermPerUm2": 0.0122676, + "conditions": { + "temperatureK": 293.15 + }, + "source": "common vendor Sellmeier set; the original code did not record the vendor", + "status": "unverified", + "uncertainty": "differs from H. S. Shi et al., J. Synthetic Cryst. 30, 85 (2001) at 20 C (refractiveindex.info main/YVO4/Shi-e-20C: A 4.607200, B 0.108087, C 0.052495, D 0.014305 in the same form) by -9.6e-4 at 633 nm, -9.2e-4 at 1064 nm; the birefringence differs by ~0.16 %; decision of the owner (2026-11-11): the vendor set stays" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/YVO4.json b/src/UltrafastEngine.Core/Data/materials/solids/YVO4.json new file mode 100644 index 0000000..e53b1b5 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/YVO4.json @@ -0,0 +1,33 @@ +{ + "id": "YVO4", + "kind": "solid", + "listOrder": 26, + "aliases": [ + "YVO4-O" + ], + "description": "Yttrium orthovanadate, ordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "vendorSellmeier", + "referenceMedium": "air", + "name": "YVO4 ordinary (common vendor Sellmeier)", + "a": 3.77834, + "numeratorUm2": 0.069736, + "poleUm2": 0.04724, + "irTermPerUm2": 0.0108133, + "conditions": { + "temperatureK": 293.15 + }, + "source": "common vendor Sellmeier set; the original code did not record the vendor", + "status": "unverified", + "uncertainty": "differs from H. S. Shi et al., J. Synthetic Cryst. 30, 85 (2001) at 20 C (refractiveindex.info main/YVO4/Shi-o-20C: A 3.778790, B 0.070479, C 0.045731, D 0.009701 in the same form) by -5.4e-4 at 633 nm, -5.9e-4 at 1064 nm; decision of the owner (2026-11-11): the vendor set stays" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/aBBO-e.json b/src/UltrafastEngine.Core/Data/materials/solids/aBBO-e.json new file mode 100644 index 0000000..823fec0 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/aBBO-e.json @@ -0,0 +1,31 @@ +{ + "id": "aBBO-e", + "kind": "solid", + "listOrder": 29, + "aliases": [], + "description": "alpha-BBO (alpha-barium borate), extraordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "vendorSellmeier", + "referenceMedium": "air", + "name": "alpha-BBO extraordinary (vendor Sellmeier)", + "a": 2.31197, + "numeratorUm2": 0.01184, + "poleUm2": 0.01607, + "irTermPerUm2": 0.004, + "conditions": { + "temperatureK": 293.15 + }, + "source": "vendor Sellmeier set; the original code did not record the vendor", + "status": "unverified", + "uncertainty": "not stated in the original code" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Data/materials/solids/aBBO.json b/src/UltrafastEngine.Core/Data/materials/solids/aBBO.json new file mode 100644 index 0000000..d90bd12 --- /dev/null +++ b/src/UltrafastEngine.Core/Data/materials/solids/aBBO.json @@ -0,0 +1,36 @@ +{ + "id": "aBBO", + "kind": "solid", + "listOrder": 28, + "aliases": [ + "ABBO", + "ALPHA-BBO", + "ABBO-O", + "A-BBO" + ], + "description": "alpha-BBO (alpha-barium borate), ordinary ray", + "notes": "n2 not set (give one if it matters)", + "quantities": { + "index": { + "model": "vendorSellmeier", + "referenceMedium": "air", + "name": "alpha-BBO ordinary (vendor Sellmeier)", + "a": 2.67579, + "numeratorUm2": 0.02099, + "poleUm2": 0.0047, + "irTermPerUm2": 0.00528, + "conditions": { + "temperatureK": 293.15 + }, + "source": "vendor Sellmeier set; the original code did not record the vendor", + "status": "unverified", + "uncertainty": "not stated in the original code" + }, + "n2": { + "valueM2PerW": 0.0, + "source": "not set: no value was entered for this material (give one if it matters)", + "status": "unverified", + "uncertainty": "0 means not set, not a measured zero" + } + } +} diff --git a/src/UltrafastEngine.Core/Engine/Compiler.cs b/src/UltrafastEngine.Core/Engine/Compiler.cs index 417d8ae..75662b9 100644 --- a/src/UltrafastEngine.Core/Engine/Compiler.cs +++ b/src/UltrafastEngine.Core/Engine/Compiler.cs @@ -76,12 +76,20 @@ public sealed class CompiledElement /// level is above grid.satelliteLevel. An element that sets only counts as one /// satellite of level 1 (always given room). public List<(double DelayS, double Level)> Satellites { get; } = new List<(double, double)>(); + /// Magnitude (s^2) of a spectral phase the element programs without declaring a group delay (an AOPDF or pulse shaper + /// keeps GroupDelayAt = 0 so the arrival-time bookkeeping is unchanged). The grid planner stretches the pulse by + /// GDD x bandwidth for it; contributions of several elements are added (worst case). + public double ProgrammedGddS2 { get; set; } /// Refractive index the beam sees inside the element (for Gaussian beam tracking); NaN = unknown (free space). public double TrackingIndex { get; set; } = double.NaN; /// Thin lenses inside the element for Gaussian beam tracking: position from the entrance (m) and power /// 1/f (1/m, vacuum-equivalent, from the optical path difference), e.g. the slices of a thermal lens. The tracker /// crosses the element at and applies each lens at its position. public List<(double ZM, double InvFocalM)> TrackLenses { get; set; } + /// Distributed lenses inside the element for Gaussian beam tracking: start from the entrance (m), length (m) and the + /// thin-lens-equivalent power 1/f (1/m) of each segment, e.g. the slices of a thermal lens. The tracker crosses each as a + /// graded-index duct n(r) = n0 (1 - g² r² / 2) with g² = (1/f) / (n0 L) (). + public List<(double ZM, double LengthM, double InvFocalM)> TrackDucts { get; set; } /// Passes through (a multipass amplifier) and the free path between them. public int TrackPasses { get; set; } = 1; public double TrackPassGapM { get; set; } @@ -373,12 +381,23 @@ public static CompiledRun Compile(SystemDescription sys, RunSpec run, ModelRegis foreach (var r in c.Ov.Rules.Where(r => r.Target.StartsWith("material:", StringComparison.OrdinalIgnoreCase))) if (!c.Out.Materials.Keys.Any(k => r.Matches("material", k, null))) c.Out.Warnings.Add("Override " + r.Target + " / " + r.Property + " (" + r.Origin + "): material not used in this system."); + // strict materials: a run built on catalogue values that are not verified is refused (physics.strictMaterials or ULTRAFAST_STRICT_MATERIALS=1) + if (run.Physics.StrictMaterials || Environment.GetEnvironmentVariable("ULTRAFAST_STRICT_MATERIALS") == "1") + { + var allowed = run.Physics.StrictMaterialsAllow; + bool Accepted(string path) => allowed.Any(a => path.Equals(a, StringComparison.OrdinalIgnoreCase) || path.StartsWith(a.TrimEnd('.') + ".", StringComparison.OrdinalIgnoreCase)); + var open = c.Out.Provenance.UnverifiedInputs.Where(u => u.Catalogue.Status != CatalogueStatus.Verified && !Accepted(u.Catalogue.Path)).ToList(); + if (open.Count > 0) + throw new ModelException("strict materials: the run uses " + open.Count + " catalogue value" + (open.Count == 1 ? "" : "s") + " that " + (open.Count == 1 ? "is" : "are") + " not verified:\n " + + string.Join("\n ", open.Select(u => u.Material + "." + u.Property.Replace(" (catalogue)", "") + " [" + u.Catalogue.Path + "] " + u.Catalogue.Status.ToString().ToLowerInvariant() + ": " + u.Catalogue.Source)) + + "\nOverride each value (docs/reference/materials.md#the-material-catalogue), put a verified one in your own catalogue file, , accept one by its path in physics.strictMaterialsAllow, or switch physics.strictMaterials off."); + } return c.Out; } private static CompilerServices Services(Ctx c) => new CompilerServices { - System = c.Sys, Run = c.Run, Registry = c.Reg, Provenance = c.Out.Provenance, Omega0 = c.Omega0, Warnings = c.Out.Warnings, + System = c.Sys, Run = c.Run, Registry = c.Reg, Provenance = c.Out.Provenance, Omega0 = c.Omega0, Warnings = c.Out.Warnings, Notes = c.Out.Notes, TiltXRad = c.TiltX, FedTiltX = (id, port) => { @@ -646,7 +665,7 @@ private static CompiledElement CompileElement(Ctx c, SystemElement e) if (Plugins.PluginManager.TryGetElementCompiler(e.Type, out var pc)) { pc(Services(c), e, ce); break; } if (string.Equals(e.Type, "transform", StringComparison.OrdinalIgnoreCase)) throw new JsonException("element '" + e.Id + "' is a \"transform\", which needs the fitting plugin (not installed: see `ultrafast plugins list` and docs/plugins/installing.md)."); - throw new JsonException("unknown element type \"" + e.Type + "\". Known types: " + string.Join(", ", ElementTypes.Concat(Plugins.PluginManager.ElementTypes)) + "."); + throw new JsonException(UnknownElementMessage(e.Type, e.Id)); } if ((type == "bulk" || type == "crystal" || type == "gain" || type == "amplifier" || type == "lens") && e.Json.Has("ghosts")) AddElementGhosts(c, e, ce, type); @@ -824,6 +843,24 @@ private static void TrackBeamCore(Ctx c, SystemElement e, CompiledElement ce, st break; } default: + if (ce.TrackDucts != null && ce.LengthM > 0) + { + double n = ce.TrackingIndex > 0 ? ce.TrackingIndex : 1.0; + for (int pass = 0; pass < ce.TrackPasses; pass++) + { + double z = 0; + foreach (var (z0, len, invF) in ce.TrackDucts.OrderBy(d => d.ZM)) + { + double zs = Math.Max(z, Math.Min(ce.LengthM, z0)); + t.Propagate(zs - z, n); z = zs; + double l = Math.Max(0, Math.Min(ce.LengthM - z, len)); + if (l > 0) { t.Duct(l, n, invF / (n * l), invF / (n * l)); z += l; } + } + t.Propagate(ce.LengthM - z, n); + if (pass < ce.TrackPasses - 1 && ce.TrackPassGapM > 0) t.Propagate(ce.TrackPassGapM, 1.0); + } + break; + } if (ce.TrackLenses != null && ce.LengthM > 0) { double n = ce.TrackingIndex > 0 ? ce.TrackingIndex : 1.0; @@ -895,6 +932,44 @@ private static void CompileDispersion(Ctx c, SystemElement e, CompiledElement ce } } + /// Lists, in the provenance and the run notes, the catalogue values of the material that a nonlinear element really used + /// (a value the run spec or the system file overrides is not listed). + private static void ReportNonlinearCatalogue(Ctx c, SystemElement e, Material m, NonlinearMedium med, bool n2Overridden, bool kerrOverridden, bool ramanOverridden) + { + var prov = c.Out.Provenance; string target = "element:" + e.Id; + void U(string prop, string key) { if (m.Used.TryGetValue(key, out var q)) prov.UseCatalogue(target, prop, q, m.Name, c.Out.Notes); } + if (!n2Overridden) U("n2", "n2"); + if (!kerrOverridden && med.KerrAnisotropy != 0) U("kerrAnisotropy", "kerrAnisotropy"); + if (!ramanOverridden && med.EffectiveRamanFraction > 0) { U("raman", "raman"); U("ramanFraction", "ramanFraction"); } + if (med.Rotational != null && c.Run.Physics.Raman) + foreach (var key in m.Used.Keys.Where(k => k.StartsWith("rotation:", StringComparison.Ordinal) || k.StartsWith("dephasing:", StringComparison.Ordinal)).ToList()) U(key.Replace(':', '.'), key); + } + + /// The ionisation data (band gap or ionisation potential, ...) come from the catalogue unless the ionization block gives them itself. + private static void ReportIonizationCatalogue(Ctx c, SystemElement e, Material m, JsonObject spec) + { + var prov = c.Out.Provenance; string target = "element:" + e.Id; + if (m.IsGas) + { + if (spec != null && spec.Has("ionizationEv")) return; + var species = string.Equals(m.GasSpecies, "Air", StringComparison.OrdinalIgnoreCase) ? GasData.AirComposition.Select(x => x.Species) : new[] { m.GasSpecies }; + foreach (var sp in species) { var q = GasData.Get(sp)?.Entry?.TryQ("ionization"); if (q != null) prov.UseCatalogue(target, "ionization", q, sp, c.Out.Notes); } + } + else if (m.Ionization == null) + { + // which of the five inputs the run gives itself; the rest come from the catalogue (or, for a material without data, from the generic defaults) + var own = new[] { "bandGapEv", "effectiveMass", "neutralDensityM3", "collisionTimeS", "recombinationTimeS" }.Where(k => spec != null && spec.Has(k)).ToList(); + var q = SolidIonizationData.CatalogueQuantityOf(m.Name); + string what = q != null ? "catalogue" : "generic default"; + if (q == null) q = SolidIonizationData.DefaultsQuantity; + if (own.Count < 5 && (q != SolidIonizationData.DefaultsQuantity || own.Count > 0)) + { + if (own.Count > 0) c.Out.Notes.Add("ionization of '" + m.Name + "': the run gives " + string.Join(", ", own) + "; the other inputs are the " + what + " values (" + q.Path + ")."); + prov.UseCatalogue(target, "ionization", q, m.Name, c.Out.Notes); + } + } + } + private static LinearTerm MakeLoss(double[] omega, double[] powerT, string mechanism, string description) { if (powerT.All(v => Math.Abs(v - powerT[0]) < 1e-12)) return new FlatLossTerm(powerT[0], mechanism, description); @@ -935,6 +1010,7 @@ private static void CompileBulk(Ctx c, SystemElement e, CompiledElement ce, bool background.Add(t); c.Out.Provenance.Record("element:" + e.Id, "dispersion", "material " + m.Name + " (" + m.Index.Description + "), L = " + ce.LengthM.ToString("G6", CultureInfo.InvariantCulture) + " m", gd != null ? idxSrc : ModelSource.MaterialDefault, gd != null ? "material index overridden -> traced data ignored" : "material"); + if (m.Used.TryGetValue("index", out var indexQ)) c.Out.Provenance.UseCatalogue("element:" + e.Id, "index", indexQ, m.Name, c.Out.Notes); } var tr = c.Sys.PerWavelength(e, "transmission"); if (tr != null) background.Add(MakeLoss(tr.Value.Omega, tr.Value.Values, "absorption", ce.Label + " internal transmission")); @@ -1009,6 +1085,7 @@ private static void CompileBulk(Ctx c, SystemElement e, CompiledElement ce, bool var pr = PlasmaResponse.For(m, c.Omega0, ionV.Kind == JsonKind.Object ? ionV.AsObject() : null, out _); med.ExtraResponses.Add(pr); c.Out.Provenance.Record("element:" + e.Id, "ionization", pr.Description, ionOv?.Source ?? ModelSource.SystemData, ionOv?.Origin ?? (e.Json.Has("ionization") ? "system file" : "run spec physics.ionization")); + ReportIonizationCatalogue(c, e, m, ionV.Kind == JsonKind.Object ? ionV.AsObject() : null); } bool wantNl = c.Run.Physics.Nonlinear && Bool(c, e, "nonlinear", !isGap) && med.IsActive && ce.LengthM > 0; // gaps are linear unless asked @@ -1034,6 +1111,7 @@ private static void CompileBulk(Ctx c, SystemElement e, CompiledElement ce, bool ce.Active = sb; c.Out.Provenance.Record("element:" + e.Id, "transverse", tv.Model + (tv.Model == "radial" ? " (" + tv.Points + " points)" : " (" + tv.Nx + " x " + tv.Ny + ")") + ", transport " + tv.Transport, tvOv?.Source ?? ModelSource.SystemData, tvOv?.Origin ?? "system file"); if (wantNl && med.EffectiveRamanFraction > 0) c.Out.Provenance.Record("element:" + e.Id, "raman", med.Raman.Description + ", fR = " + med.RamanFraction.ToString("G3", CultureInfo.InvariantCulture), rr?.Source ?? ModelSource.MaterialDefault, rr?.Origin ?? "material " + m.Name); + if (wantNl) ReportNonlinearCatalogue(c, e, m, med, n2r != null, kar != null || e.Json.Has("kerrAnisotropy"), rr != null); return; } IBeamModel beam = null; @@ -1071,6 +1149,7 @@ private static void CompileBulk(Ctx c, SystemElement e, CompiledElement ce, bool seg.Background.AddRange(background); ce.Nonlinear = seg; if (med.EffectiveRamanFraction > 0) c.Out.Provenance.Record("element:" + e.Id, "raman", med.Raman.Description + ", fR = " + med.RamanFraction.ToString("G3", CultureInfo.InvariantCulture), rr?.Source ?? ModelSource.MaterialDefault, rr?.Origin ?? "material " + m.Name); + ReportNonlinearCatalogue(c, e, m, med, n2r != null, kar != null || e.Json.Has("kerrAnisotropy"), rr != null); } else ce.Pre.AddRange(background); } @@ -1463,6 +1542,17 @@ internal static double PeakPowerAfter(PulseField p0, IList> return Math.Min(pk, Results.PulseMetrics.TransformLimitedPower(p0).Max() * 1.0001); } + /// Element types of the first-party ao plugin, named in the unknown-element message when the plugin is missing. + private static readonly string[] AoElementTypes = { "deformableMirror", "slm", "spectralShaper", "aopdf" }; + + /// The error for an element type nobody registered: names the ao plugin for its four types, else lists the known types. + public static string UnknownElementMessage(string type, string id) + { + if (type != null && AoElementTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) + return "element '" + id + "' is a \"" + type + "\", which needs the ao plugin (not installed or not compiled into this library: see `ultrafast plugins list` and docs/plugins/installing.md)."; + return "unknown element type \"" + type + "\". Known types: " + string.Join(", ", ElementTypes.Concat(Plugins.PluginManager.ElementTypes)) + "."; + } + /// Delay room (s) the window needs for the satellites of : the largest delay at /// which the satellites of all elements together can reach relative intensity. /// The satellites of successive elements combine (a ghost of a ghost); satellites that land at the same delay @@ -1556,7 +1646,9 @@ private static void BuildGridAndPulse(Ctx c) { // one spectral bin: no pulse to fit, nothing to plan; the window only sets the energy (power x window) double win = double.IsNaN(run.Grid.WindowS) ? Pulses.PulseSpec.CwWindowS : run.Grid.WindowS; - c.Out.Grid = GridPlanner.Create(64, win, c.Omega0, true, out double wc); + // a single line: the lattice is shifted so that a bin sits on the requested carrier (a pulse is built at the requested carrier as an offset of the snapped + // lattice, a single line cannot be); carrier-resolved models need the snapped lattice and are reached with an explicit snapCarrier: true + c.Out.Grid = GridPlanner.Create(64, win, c.Omega0, run.Grid.SnapCarrier && run.Grid.SnapCarrierGiven, out double wc); c.Out.Input = ps.Build(c.Out.Grid); c.Out.Notes.Add(string.Format(CultureInfo.InvariantCulture, "Cw source: {0:G4} W over a {1:G4} s window, one spectral bin at {2:F4} nm. Fluences are intensity x window.", c.Out.Input.TotalEnergyJ() / win, win, 2 * Math.PI * PhysicalConstants.SpeedOfLightMPerS / wc * 1e9)); return; @@ -1620,7 +1712,8 @@ private static void BuildGridAndPulse(Ctx c) memoryS = Math.Max(memoryS, GridPlanner.ResponseMemoryS(x.Nonlinear.Medium.Raman)); if (memoryS > 5 * tl) c.Out.Notes.Add(string.Format(CultureInfo.InvariantCulture, "A delayed response in the system keeps the index changed for {0:G3} s after the pulse: the window is sized to hold it.", memoryS)); var plan = GridPlanner.Plan(p0, tau, margin, anyNl ? run.Grid.MinSpanFraction : 0.0, window0, run.Grid.ExplicitResolution ? run.Grid.N : n0, run.Grid.MaxN, run.Grid.Auto || run.Grid.ExtendForSatellites, run.Grid.ExplicitResolution, Results.PulseMetrics.Fwhm(g0.Time, Results.PulseMetrics.TransformLimitedPower(p0)) / run.Grid.MinPointsPerFwhm, - (run.Grid.TailEnabled ? 0.0 : SatelliteRoom(c.Out.Elements.Where(x => x.Enabled), run.Grid.SatelliteLevel, c.Out.Notes)) + run.Pulse.SatelliteHalfWindowS() + memoryS); + (run.Grid.TailEnabled ? 0.0 : SatelliteRoom(c.Out.Elements.Where(x => x.Enabled), run.Grid.SatelliteLevel, c.Out.Notes)) + run.Pulse.SatelliteHalfWindowS() + memoryS, + c.Out.Elements.Where(x => x.Enabled).Sum(x => Math.Abs(x.ProgrammedGddS2))); if (run.Grid.TailEnabled) c.Out.Notes.Add("grid.tail: ghosts, echoes and other satellites are carried on the long tail grid; the main window is sized for the pulse only."); c.Out.GridPlan = plan; c.Out.Notes.AddRange(plan.Notes); @@ -1656,6 +1749,7 @@ internal CompilerServices() { } public ProvenanceLog Provenance { get; internal set; } public double Omega0 { get; internal set; } public List Warnings { get; internal set; } + public List Notes { get; internal set; } /// Carrier arriving on an element's input port, from what feeds it (rad/s); NaN if that is not compiled yet. internal Func FedCarrier; /// Lab tilt (rad, x) arriving on an input port, and on the element's main input. diff --git a/src/UltrafastEngine.Core/Engine/CompilerModulator.cs b/src/UltrafastEngine.Core/Engine/CompilerModulator.cs index 73cb252..7ed929f 100644 --- a/src/UltrafastEngine.Core/Engine/CompilerModulator.cs +++ b/src/UltrafastEngine.Core/Engine/CompilerModulator.cs @@ -65,7 +65,7 @@ double[] Map(string key, string fileKey) m.OffsetXM = Num(c, e, "offsetXM", 0); m.OffsetYM = Num(c, e, "offsetYM", 0); m.RotationRad = Num(c, e, "rotationRad", 0); m.DesignOmega = e.Json.Has("designWavelengthM") ? 2 * Math.PI * PhysicalConstants.SpeedOfLightMPerS / Num(c, e, "designWavelengthM", 0) : c.Omega0; string chrom = Str(c, e, "chromatic", "opd").ToLowerInvariant(); - if (chrom == "phaseatdesign") chrom = "opd"; // the plan's name (plan-ao-digital-twin-2026-11.md): a phase given at the design wavelength, scaled with omega + if (chrom == "phaseatdesign") chrom = "opd"; // the plan's name (history.md): a phase given at the design wavelength, scaled with omega if (chrom != "opd" && chrom != "fixedphase" && chrom != "index") throw new JsonException(origin + ": chromatic must be \"opd\" (also \"phaseAtDesign\"), \"fixedPhase\" or \"index\"."); m.FixedPhase = chrom == "fixedphase"; if (m.FixedPhase && m.OpdM != null) throw new JsonException(origin + ": an optical path (opdM) is chromatic; use phaseRad with \"fixedPhase\"."); diff --git a/src/UltrafastEngine.Core/Engine/Executor.cs b/src/UltrafastEngine.Core/Engine/Executor.cs index e61eaf6..79c6cbf 100644 --- a/src/UltrafastEngine.Core/Engine/Executor.cs +++ b/src/UltrafastEngine.Core/Engine/Executor.cs @@ -513,7 +513,7 @@ public static FrequencyGrid PlanDownstreamGrid(CompiledRun cr, PulseField p, int double window = g.N * g.DTime; double fwhmTl = PulseMetrics.Fwhm(g.Time, PulseMetrics.TransformLimitedPower(p)); var plan = GridPlanner.Plan(p, tau, margin, anyNl ? run.Grid.MinSpanFraction : 0.0, window, g.N, run.Grid.MaxN, true, false, - double.IsNaN(fwhmTl) ? double.PositiveInfinity : fwhmTl / run.Grid.MinPointsPerFwhm, Compiler.SatelliteRoom(rest, run.Grid.SatelliteLevel)); + double.IsNaN(fwhmTl) ? double.PositiveInfinity : fwhmTl / run.Grid.MinPointsPerFwhm, Compiler.SatelliteRoom(rest, run.Grid.SatelliteLevel), rest.Sum(x => Math.Abs(x.ProgrammedGddS2))); double reqW = Math.Max(plan.RequiredWindowS, 1e-15), reqDt = plan.RequiredDtS; int a = (int)Math.Ceiling(Math.Log(reqW / window, 2) - 1e-9); int b = (int)Math.Floor(Math.Log(reqDt / g.DTime, 2) + 1e-9); diff --git a/src/UltrafastEngine.Core/Engine/GraphExecutor.cs b/src/UltrafastEngine.Core/Engine/GraphExecutor.cs index f572189..d2c24c8 100644 --- a/src/UltrafastEngine.Core/Engine/GraphExecutor.cs +++ b/src/UltrafastEngine.Core/Engine/GraphExecutor.cs @@ -10,7 +10,7 @@ // to the port it is linked to. A loop repeats its stage: packets sent back to an earlier element wait in that element's queue // until the next pass, so no edge has to be cut by hand. // -// Design: docs/design/branching.md, sections 5 and 7. +// Reference: docs/reference/branches.md. using System; using System.Collections.Generic; diff --git a/src/UltrafastEngine.Core/Engine/GridPlanner.cs b/src/UltrafastEngine.Core/Engine/GridPlanner.cs index a2922b7..3f94a8d 100644 --- a/src/UltrafastEngine.Core/Engine/GridPlanner.cs +++ b/src/UltrafastEngine.Core/Engine/GridPlanner.cs @@ -37,7 +37,7 @@ public static (double Lo, double Hi) SpectralExtent(PulseField pulse, double rel return (lo, hi); } - public static GridPlan Plan(PulseField pulse, IReadOnlyList> elementGroupDelays, double spectralMargin, double minSpanFraction, double configuredWindowS, int configuredN, int maxN, bool autoSize, bool keepConfiguredDt = true, double maxDtS = double.PositiveInfinity, double satelliteDelayS = 0) + public static GridPlan Plan(PulseField pulse, IReadOnlyList> elementGroupDelays, double spectralMargin, double minSpanFraction, double configuredWindowS, int configuredN, int maxN, bool autoSize, bool keepConfiguredDt = true, double maxDtS = double.PositiveInfinity, double satelliteDelayS = 0, double programmedGddS2 = 0) { var g = pulse.Grid; var plan = new GridPlan { WindowS = configuredWindowS, N = configuredN }; @@ -71,7 +71,10 @@ public static GridPlan Plan(PulseField pulse, IReadOnlyList while (b > 0 && p[b] < 1e-6 * pk) b--; double inputExtent = Math.Max(g.DTime, (b - a) * g.DTime); // satellite pulses (ghost replicas, pre-pulse partners) must fit too: room on both sides - double halfNeeded = Math.Max(Math.Abs(tMin), Math.Abs(tMax)) + 0.5 * inputExtent + Math.Abs(satelliteDelayS); + // a programmed spectral phase that carries no declared group delay (shaper, AOPDF) still stretches the pulse by GDD x bandwidth + double stretchS = Math.Abs(programmedGddS2) * (wHi - wLo); + if (stretchS > 0) plan.Notes.Add(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Programmed GDD ({0:G4} fs^2) over the {1:G4} rad/s input bandwidth stretches the pulse by up to {2:G4} ps.", Math.Abs(programmedGddS2) * 1e30, wHi - wLo, stretchS * 1e12)); + double halfNeeded = Math.Max(Math.Abs(tMin), Math.Abs(tMax)) + 0.5 * inputExtent + Math.Abs(satelliteDelayS) + 0.5 * stretchS; plan.RequiredWindowS = 2.0 * 1.25 * halfNeeded; double devMax = Math.Max(g.OmegaCenter - wLo, wHi - g.OmegaCenter) * Math.Max(1.0, spectralMargin); diff --git a/src/UltrafastEngine.Core/Engine/MultiPort.cs b/src/UltrafastEngine.Core/Engine/MultiPort.cs index 7f052d0..5ce40e9 100644 --- a/src/UltrafastEngine.Core/Engine/MultiPort.cs +++ b/src/UltrafastEngine.Core/Engine/MultiPort.cs @@ -17,7 +17,7 @@ // An element runs once per packet per input port. That is exact for a linear element, by superposition: interference // appears when the packets on one output merge. Merging puts them in a common frame, from their ledgers, and takes the // overlap of their modes: tracked Gaussians analytically, carried profiles by adding the fields point by point. -// Design: docs/design/branching.md, sections 5 and 6. +// Reference: docs/reference/branches.md. using System; using System.Collections.Generic; diff --git a/src/UltrafastEngine.Core/Engine/PathLedger.cs b/src/UltrafastEngine.Core/Engine/PathLedger.cs index b02c773..126bcf4 100644 --- a/src/UltrafastEngine.Core/Engine/PathLedger.cs +++ b/src/UltrafastEngine.Core/Engine/PathLedger.cs @@ -14,7 +14,7 @@ // void Add(DispersionPoint own) add one element's phase and delay // // The true field is the stored one times exp(i (PhaseRad + (omega - omega0) FrameDelayS)). -// Design: docs/design/branching.md, section 6. +// Reference: docs/reference/branches.md. using UltrafastEngine.Physics; diff --git a/src/UltrafastEngine.Core/Gain/AseModel.cs b/src/UltrafastEngine.Core/Gain/AseModel.cs new file mode 100644 index 0000000..d156ee9 --- /dev/null +++ b/src/UltrafastEngine.Core/Gain/AseModel.cs @@ -0,0 +1,206 @@ +// AseModel.cs -- amplified spontaneous emission over the inversion lifetime (quasi-static, both directions, spectrally resolved). +// +// What it does. Between pulses (milliseconds) the upper-state density N2(z, cell) is steady on the time scale of the ASE +// transit (picoseconds), so the ASE is solved as a boundary-value problem in z at frozen N2 and the result depletes N2 in the rate +// equations of GainCrystal (PumpCw, Decay). Per transverse cell c (1-D: one cell) and per frequency nu_i of the emission band the +// intensity I (W / m^2 / Hz) in a bundle of etendue q = n^2 Omega / lambda0^2 (1 / m^2) grows as +// dI+- / dz = +- gamma I + p h nu sigma_e N2 q, gamma = sigma_e N2 - sigma_a (N_t - N2) +// (p = 1 or 2 polarisations). Forward I+ starts at 0 at z = 0, backward I- at z = L. For q = 1 / A_mode and p = 2 this is the +// textbook single-mode ASE of an amplifier, P = n_sp h nu dnu (G - 1) per polarisation (Desurvire 1994; Siegman 1986, ch. 6); for +// Omega = 4 pi the source integrates to N2 h nu / tau_rad by the Fuchtbauer-Ladenburg relation (Aull and Jenssen 1982), which fixes the factors. +// +// Classes: +// AseSpec -- the user's settings (solid angle, polarisations, band, points). +// AseModel -- the solver: Rates(...) gives the extra decay rate of every (slice, cell) for GainCrystal; ModeOutput(...) gives the +// ASE leaving the signal mode (for the contrast pedestal); Slice(...) is the exact slice step (public for tests). +// Key signatures: +// public double[] Rates(double[] n2, double[] nt) -> W[k] (1/s) = stimulated photons lost per ion per second +// public AseOutput ModeOutput(double[] n2, double[] nt, int cell) -> forward / backward power spectra in the signal mode +// public static double Slice(double iIn, double gamma, double source, double dz, out double stim) -> I after one slice, exact for constant gamma +// Not modelled: transverse spreading of the rays between cells (each cell is an independent column), reflections at the faces +// (so no parasitic lasing), pump-light fluorescence trapping. See docs/reference/gain.md#ase. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UltrafastEngine.Serialization; + +namespace UltrafastEngine.Gain +{ + /// Settings of the inter-pulse ASE model (aseModel on a gain or amplifier element). + public sealed class AseSpec + { + public bool Enabled; + /// Effective solid angle (sr, inside the medium) of the rays that depopulate the inversion; NaN = min(pi r_pump^2 / L^2, escape cone). + public double SolidAngleSr = double.NaN; + public int Polarizations = 2; + public int Points = 40; + public double BandLowerM = double.NaN, BandUpperM = double.NaN; + + public static AseSpec FromJson(JsonValue v) + { + var s = new AseSpec(); + if (v == null || v.IsNull) return s; + if (v.Kind == JsonKind.Bool) { s.Enabled = v.AsBool(); return s; } + var o = v.AsObject(); + s.Enabled = o.GetBool("enabled", true); + s.SolidAngleSr = o.GetDouble("solidAngleSr", double.NaN); + s.Polarizations = o.GetInt("polarizations", 2); + s.Points = o.GetInt("points", 40); + s.BandLowerM = o.GetDouble("bandLowerM", double.NaN); s.BandUpperM = o.GetDouble("bandUpperM", double.NaN); + if (s.Polarizations != 1 && s.Polarizations != 2) throw new ArgumentException("aseModel.polarizations must be 1 or 2"); + if (s.Points < 4) throw new ArgumentException("aseModel.points must be at least 4"); + return s; + } + } + + /// ASE leaving the signal mode at the two end faces, per frequency (W / Hz), and its integral. + public sealed class AseOutput + { + public double[] NuHz, WavelengthM, ForwardWPerHz, BackwardWPerHz; + public double ForwardW, BackwardW; + } + + public sealed class AseModel + { + const double H = GainMedium.H, C = GainMedium.C; + private readonly GainCrystal _gc; + public readonly AseSpec Spec; + public double[] Nu { get; } // Hz + public double DNu { get; } // Hz + public double[] SigmaE { get; } + public double[] SigmaA { get; } + public double HostIndex { get; } + public double SolidAngleSr { get; } + /// Last evaluation: ASE power added by stimulated emission summed over the crystal (W) and the photon rate (1/s). + public double LastStimulatedPowerW { get; private set; } + public double LastStimulatedPhotonsPerS { get; private set; } + + /// Tests: use these frequencies (Hz) with spacing instead of the band of the medium. + public AseModel(GainCrystal gc, AseSpec spec, double hostIndex, double signalOmega, double[] nuOverride = null, double dnuOverride = double.NaN) + { + _gc = gc; Spec = spec; HostIndex = hostIndex; + var m = gc.Medium; + if (nuOverride != null) { Nu = nuOverride; DNu = dnuOverride; } + else + { + double ls = 2 * Math.PI * C / signalOmega, lo = double.IsNaN(spec.BandLowerM) ? 0.75 * ls : spec.BandLowerM, hi = double.IsNaN(spec.BandUpperM) ? 1.35 * ls : spec.BandUpperM; + // the band where the emission cross section matters: sigma_e above 1e-3 of its maximum + int ns = 1201; var lam = new double[ns]; var se = new double[ns]; double mx = 0; + for (int i = 0; i < ns; i++) { lam[i] = lo + (hi - lo) * i / (ns - 1); se[i] = m.SigmaE(GainMedium.Omega(lam[i])); mx = Math.Max(mx, se[i]); } + int a = 0, b = ns - 1; + if (double.IsNaN(spec.BandLowerM)) while (a < b && se[a] < 1e-3 * mx) a++; + if (double.IsNaN(spec.BandUpperM)) while (b > a && se[b] < 1e-3 * mx) b--; + double nuLo = C / lam[b], nuHi = C / lam[a]; + if (!(nuHi > nuLo)) { nuLo = C / hi; nuHi = C / lo; } + int K = spec.Points; DNu = (nuHi - nuLo) / K; + Nu = Enumerable.Range(0, K).Select(i => nuLo + (i + 0.5) * DNu).ToArray(); + } + SigmaE = Nu.Select(v => m.SigmaE(2 * Math.PI * v)).ToArray(); + SigmaA = Nu.Select(v => m.SigmaA(2 * Math.PI * v)).ToArray(); + if (!double.IsNaN(spec.SolidAngleSr)) SolidAngleSr = spec.SolidAngleSr; + else + { + double rp = gc.Pump.RadiusM > 0 ? gc.Pump.RadiusM : Math.Sqrt(gc.CellArea.Sum() / Math.PI); + double geo = Math.PI * rp * rp / (gc.LengthM * gc.LengthM), esc = 2 * Math.PI * (1 - Math.Sqrt(1 - 1 / (hostIndex * hostIndex))); + SolidAngleSr = Math.Min(geo, esc); + } + } + + /// Etendue factor q = n^2 Omega / lambda0^2 (1 / m^2) at frequency nu for the bundle of solid angle omega (sr inside the medium). + public double Etendue(double nu, double omegaSr) { double l0 = C / nu; return HostIndex * HostIndex * omegaSr / (l0 * l0); } + + /// + /// One slice of length dz at constant gain gamma (1/m) and source S (W / m^3 / Hz, per m of path): I_out = I_in e^{gamma dz} + S (e^{gamma dz} - 1) / gamma. + /// stim = I_out - I_in - S dz, the intensity added by stimulated emission (net of absorption): the photon balance of the slice. + /// + public static double Slice(double iIn, double gamma, double source, double dz, out double stim) + { + double x = gamma * dz, e = Math.Exp(x); + double iOut = iIn * e + source * dz * (Math.Abs(x) < 1e-8 ? 1 + 0.5 * x : (e - 1) / x); + stim = iOut - iIn - source * dz; + return iOut; + } + + /// Integrate one column (cell) at frozen N2: I+ and I- at the entrance of every slice j are not needed, only the stimulated photons per slice. + /// Returns the stimulated photons per slice (1/s/m^2 of the column, J-free: W / m^2 divided by h nu, summed over nu) and the end intensities. + private void Column(double[] n2, double[] nt, int cell, double omegaSr, double areaForPower, + double[] stimPhotons, double[] stimPower, double[] fwdEnd, double[] bwdEnd) + { + int Nz = _gc.Nz, Cells = _gc.Cells; double dz = _gc.Dz; int K = Nu.Length; double p = Spec.Polarizations; + if (stimPhotons != null) Array.Clear(stimPhotons, 0, Nz); + if (stimPower != null) Array.Clear(stimPower, 0, Nz); + for (int i = 0; i < K; i++) + { + double hv = H * Nu[i], q = Etendue(Nu[i], omegaSr), se = SigmaE[i], sa = SigmaA[i]; + for (int dir = 0; dir < 2; dir++) + { + double I = 0; + for (int jj = 0; jj < Nz; jj++) + { + int j = dir == 0 ? jj : Nz - 1 - jj, k = j * Cells + cell; + double N2 = n2[k], N1 = nt[k] - N2; + double gamma = se * N2 - sa * N1, src = p * hv * se * N2 * q; + I = Slice(I, gamma, src, dz, out double stim); + // stim is the W / m^2 / Hz added over dz: photons per volume per second = stim / (h nu dz) + if (stimPhotons != null) stimPhotons[j] += DNu * stim / (hv * dz); + if (stimPower != null) stimPower[j] += DNu * stim * areaForPower; + } + if (dir == 0 && fwdEnd != null) fwdEnd[i] = I * areaForPower; else if (dir == 1 && bwdEnd != null) bwdEnd[i] = I * areaForPower; + } + } + } + + /// + /// Extra decay rate W[k] (1/s) of the upper state of every (slice, cell) from the stimulated ASE photons at the given N2 (the + /// spontaneous part is already in 1 / tau): W = (stimulated photons per volume per second) / N2. Updates LastStimulated*. + /// + public double[] Rates(double[] n2, double[] nt) + { + int Nz = _gc.Nz, Cells = _gc.Cells; var W = new double[n2.Length]; + double totP = 0, totPh = 0; object lk = new object(); + Parallel.For(0, Cells, () => (0.0, 0.0), (c, _, acc) => + { + var ph = new double[Nz]; var pw = new double[Nz]; + Column(n2, nt, c, SolidAngleSr, _gc.CellArea[c], ph, pw, null, null); + double a = acc.Item1, b = acc.Item2; + for (int j = 0; j < Nz; j++) + { + int k = j * Cells + c; + W[k] = n2[k] > 0 ? ph[j] / n2[k] : 0; + a += pw[j]; b += ph[j] * _gc.CellArea[c] * _gc.Dz; + } + return (a, b); + }, acc => { lock (lk) { totP += acc.Item1; totPh += acc.Item2; } }); + LastStimulatedPowerW = totP; LastStimulatedPhotonsPerS = totPh; + return W; + } + + /// ASE leaving the signal mode (one spatial mode of area , the model's polarisations) through each end face, from the column of . + public AseOutput ModeOutput(double[] n2, double[] nt, int cell, double modeArea) + { + int K = Nu.Length; var f = new double[K]; var b = new double[K]; + // etendue of exactly one mode: n^2 Omega / lambda0^2 = 1 / A, i.e. Omega = lambda0^2 / (n^2 A); the power is intensity x A + var sav = Spec.Polarizations; + var o = new AseOutput { NuHz = Nu, WavelengthM = Nu.Select(v => C / v).ToArray(), ForwardWPerHz = f, BackwardWPerHz = b }; + int Nz = _gc.Nz, Cells = _gc.Cells; double dz = _gc.Dz, p = Spec.Polarizations; + for (int i = 0; i < K; i++) + { + double hv = H * Nu[i], se = SigmaE[i], sa = SigmaA[i], q = 1 / modeArea; + for (int dir = 0; dir < 2; dir++) + { + double I = 0; + for (int jj = 0; jj < Nz; jj++) + { + int j = dir == 0 ? jj : Nz - 1 - jj, k = j * Cells + cell; + double N2 = n2[k], N1 = nt[k] - N2; + I = Slice(I, se * N2 - sa * N1, p * hv * se * N2 * q, dz, out _); + } + if (dir == 0) f[i] = I * modeArea; else b[i] = I * modeArea; + } + } + o.ForwardW = f.Sum() * DNu; o.BackwardW = b.Sum() * DNu; + return o; + } + } +} diff --git a/src/UltrafastEngine.Core/Gain/GainCrystal.cs b/src/UltrafastEngine.Core/Gain/GainCrystal.cs index b644c2f..6610a95 100644 --- a/src/UltrafastEngine.Core/Gain/GainCrystal.cs +++ b/src/UltrafastEngine.Core/Gain/GainCrystal.cs @@ -113,6 +113,8 @@ public sealed class GainCrystal public ISpatialField DopingProfile { get; } /// Non-null: amplified spontaneous emission is seeded (1-D passes): per slice and frequency bin, hbar w sigma_e N2 dz of noise into the signal mode. public Random Ase { get; set; } + /// Non-null: inter-pulse ASE (both directions, quasi-static) depletes N2 in PumpCw, Decay and RelaxTowardsCw (AseModel.cs). + public AseModel AseDepletion { get; set; } /// 1-D: one cell representing a Gaussian signal mode of 1/e^2 radius . public GainCrystal(GainMedium m, double lengthM, int nz, PumpSpec pump, double modeRadiusM, ITransverseBasis basis, ISpatialField doping) @@ -189,12 +191,21 @@ public void PumpCw(double durationS, int steps = 60, bool tally = true) I *= t; } } - for (int k = 0; k < N2.Length; k++) + // ASE: the extra decay rate at the current N2, in sub-steps small enough that it does not change N2 appreciably within one + double[] W = AseDepletion?.Rates(N2, Nt); int sub = 1; + if (W != null) sub = (int)Math.Min(40, Math.Max(1, Math.Ceiling(dt * W.Max() / 0.3))); + for (int q = 0; q < sub; q++) { - double phi = phiF[k] + phiB[k]; - double A = _sap * Nt[k] * phi, B = (_sap + _sep) * phi + 1 / tau; - double ninf = A / B; - N2[k] = ninf + (N2[k] - ninf) * Math.Exp(-B * dt); + if (W != null && q > 0) W = AseDepletion.Rates(N2, Nt); + double h = dt / sub; + for (int k = 0; k < N2.Length; k++) + { + double phi = phiF[k] + phiB[k]; + double A = _sap * Nt[k] * phi, B = (_sap + _sep) * phi + 1 / tau + (W != null ? W[k] : 0); + B = Math.Max(B, 1e-3 / tau); + double ninf = A / B; + N2[k] = ninf + (N2[k] - ninf) * Math.Exp(-B * h); + } } } if (Basis == null && tally) @@ -245,16 +256,34 @@ public void RelaxTowardsCw(double phiSignal, double sigmaE, double sigmaA, doubl if (Pump.Passes == 2) for (int j = Nz - 1; j >= 0; j--) { int k = j * Cells + c; double al = PumpAlpha(k), t = Math.Exp(-al * Dz); phiP[k] += I * (1 - t) / Math.Max(1e-300, al * Dz) / hv; I *= t; } } + var wAse = AseDepletion?.Rates(N2, Nt); for (int k = 0; k < N2.Length; k++) { double num = (_sap * phiP[k] + sigmaA * phiSignal) * Nt[k]; - double den = (_sap + _sep) * phiP[k] + (sigmaE + sigmaA) * phiSignal + 1 / tau; + double den = (_sap + _sep) * phiP[k] + (sigmaE + sigmaA) * phiSignal + 1 / tau + (wAse != null ? wAse[k] : 0); double ss = num / den; N2[k] += a * (ss - N2[k]); } } - public void Decay(double t) { if (t <= 0) return; double f = Math.Exp(-t / Medium.LifetimeS); for (int k = 0; k < N2.Length; k++) N2[k] *= f; } + public void Decay(double t) + { + if (t <= 0) return; + if (AseDepletion == null) { double f = Math.Exp(-t / Medium.LifetimeS); for (int k = 0; k < N2.Length; k++) N2[k] *= f; return; } + // with ASE the decay rate depends on N2 itself: sub-steps of at most a tenth of the lifetime and of 0.3 / W + int steps = (int)Math.Min(500, Math.Max(1, Math.Ceiling(10 * t / Medium.LifetimeS))); double h = t / steps; + for (int s = 0; s < steps; s++) + { + var W = AseDepletion.Rates(N2, Nt); double mw = Math.Max(1e-30, W.Max()); + int sub = (int)Math.Min(40, Math.Max(1, Math.Ceiling(h * mw / 0.3))); + for (int q = 0; q < sub; q++) + { + if (q > 0) W = AseDepletion.Rates(N2, Nt); + double f = Math.Exp(-h / sub / Medium.LifetimeS); + for (int k = 0; k < N2.Length; k++) N2[k] *= f * Math.Exp(-W[k] * h / sub); + } + } + } // ------------------------------------------------------------------------------------------ diagnostics diff --git a/src/UltrafastEngine.Core/Gain/GainElements.cs b/src/UltrafastEngine.Core/Gain/GainElements.cs index 45b6a99..61b020c 100644 --- a/src/UltrafastEngine.Core/Gain/GainElements.cs +++ b/src/UltrafastEngine.Core/Gain/GainElements.cs @@ -39,6 +39,7 @@ public sealed class GainSetup public double ThermalFocalLengthM = double.NaN; // measured thermal focal length (replaces the heat model) public double TrackedThermalFocalLengthM = double.NaN; // the compile-time lens used by the tracking (for the beam) public double TrackedOnAxisFocalLengthM = double.NaN; // the compile-time on-axis f_th (pump alone), to compare with the run + public double TrackedEffectiveFocalLengthM = double.NaN; // -1/C of the tracked duct chain (one pass): the lens as a thick element public bool Stress; // thermal stress birefringence (cubic hosts) public double RepRateHz = double.NaN; public bool SteadyState = true; @@ -47,6 +48,7 @@ public sealed class GainSetup public double SingleShotPumpS = double.NaN; // cw pump time for a single shot (default: to equilibrium) public ISpatialField Doping; public bool Ase; public int AseSeed = 7; + public AseSpec AseModel = new AseSpec(); // inter-pulse ASE depleting the inversion (AseModel.cs) public TransverseSpec Transverse; public SpatialProperties Properties; @@ -214,6 +216,30 @@ public static void RecordGrin(ElementRecord r, ISpatialField field, double fth, r.Values["hostIndex"] = s.Host.IndexAt(omega); } + /// Inter-pulse ASE in the signal mode at the state the next pulse sees (values; not part of the pulse's energy ledger). + public static void RecordAse(ActiveContext ctx, GainCrystal gc, GainSetup s, double modeArea, double pumpPowerScale) + { + var m = gc.AseDepletion; if (m == null) return; + var r = ctx.Record; + var o = m.ModeOutput(gc.N2, gc.Nt, gc.AxisCell, modeArea); + r.Values["aseForwardPowerW"] = o.ForwardW; r.Values["aseBackwardPowerW"] = o.BackwardW; + r.Values["aseSolidAngleSr"] = m.SolidAngleSr; + r.AddArray("aseWavelengthM", o.WavelengthM, "m", "wavelength axis of the ASE spectrum"); + r.AddArray("aseForwardSpectrumWPerHz", o.ForwardWPerHz, "W/Hz", "forward ASE in the signal mode (both end faces: backward in aseBackwardSpectrumWPerHz), before the pulse"); + r.AddArray("aseBackwardSpectrumWPerHz", o.BackwardWPerHz, "W/Hz", "backward ASE in the signal mode"); + m.Rates(gc.N2, gc.Nt); + r.Values["aseStimulatedPowerW"] = m.LastStimulatedPowerW; + if (pumpPowerScale > 0 && gc.AbsorbedPumpJ.Sum() > 0) r.Values["aseDepletionFraction"] = m.LastStimulatedPowerW / (gc.AbsorbedPumpJ.Sum() * pumpPowerScale); + ctx.Record.Notes.Add(string.Format(CultureInfo.InvariantCulture, "inter-pulse ASE ({0} polarisation(s), {1:G3} sr, {2} frequencies): {3:G3} W forward in the signal mode, {4:G3} W taken from the inversion", m.Spec.Polarizations, m.SolidAngleSr, m.Nu.Length, o.ForwardW, m.LastStimulatedPowerW)); + } + + /// ASE energy in the signal mode per pulse period relative to the output pulse: the strength of a pedestal of kind "ase". + public static void RecordAsePedestal(ElementRecord r, double outputEnergyJ, double repRateHz) + { + if (!r.Values.TryGetValue("aseForwardPowerW", out double pw) || !(repRateHz > 0) || !(outputEnergyJ > 0)) return; + r.Values["asePedestalEnergyJ"] = pw / repRateHz; r.Values["asePedestalEnergyFraction"] = pw / repRateHz / outputEnergyJ; + } + /// Record diagnostics shared by both elements. public static void RecordBefore(ActiveContext ctx, GainCrystal gc, GainSetup s, PulseField p, double omegaSignal) { @@ -296,25 +322,71 @@ public sealed class GainElement : IActiveElement, Engine.IReportsCarrierPhase /// C_B = C_r - C_phi. An x-polarised beam filling the rod loses to y the fraction D = (1 - sin C / C) / 4, which tends to 1/4; /// this moves that fraction of the energy, and reports D and the two thermal lenses f_r, f_phi. /// - private void ApplyStress(ActiveContext ctx, Complex[] ax, ref Complex[] ay, double wmode, double w0) + /// The retardation C at the rod edge and the rod radius from the heat load of this run; false (with a warning) when the medium has no stress data. + private bool StressRetardation(ActiveContext ctx, double w0, out double C, out double r0) { - var r = ctx.Record; var m = S.Medium; - if (double.IsNaN(m.StressCr) || double.IsNaN(m.ThermalExpansionPerK)) { ctx.Result.Warnings.Add("Element '" + S.Id + "': no stress coefficients for " + m.Name + " (thermalExpansionPerK, stressCr, stressCphi): thermal birefringence is not applied."); return; } - if (!r.Values.TryGetValue("heatW", out double heat) || !(heat > 0)) return; + var r = ctx.Record; var m = S.Medium; C = 0; r0 = S.RodRadiusM > 0 ? S.RodRadiusM : 3 * S.Pump.RadiusM; + if (double.IsNaN(m.StressCr) || double.IsNaN(m.ThermalExpansionPerK)) { ctx.Result.Warnings.Add("Element '" + S.Id + "': no stress coefficients for " + m.Name + " (thermalExpansionPerK, stressCr, stressCphi): thermal birefringence is not applied."); return false; } + if (!r.Values.TryGetValue("heatW", out double heat) || !(heat > 0)) return false; double n0 = S.Host.IndexAt(w0), lam = 2 * Math.PI * GainMedium.C / w0, a = m.ThermalExpansionPerK, K = m.ThermalConductivity; - double r0 = S.RodRadiusM > 0 ? S.RodRadiusM : 3 * S.Pump.RadiusM, area = Math.PI * r0 * r0; - double C = n0 * n0 * n0 * a * (m.StressCr - m.StressCphi) * heat / (lam * K); + C = n0 * n0 * n0 * a * (m.StressCr - m.StressCphi) * heat / (lam * K); + return true; + } + + /// + /// Thermal stress birefringence of a uniformly heated rod (Koechner, Solid-State Laser Engineering, ch. 7): the retardation + /// between radial and tangential polarisation is delta = C (r / r0)^2, C = n0^3 alpha C_B P_h / (lambda K) with + /// C_B = C_r - C_phi. An x-polarised beam filling the rod loses to y the fraction D = (1 - sin C / C) / 4, which tends to 1/4. + /// Reports D and the two thermal lenses f_r, f_phi; the lumped form () moves that fraction of the energy + /// from x to y; the four-lobed pattern (ApplyStressPattern) has already been applied when it is false. + /// + private void ApplyStress(ActiveContext ctx, Complex[] ax, ref Complex[] ay, double wmode, double w0, bool rotate, double patternFraction) + { + var r = ctx.Record; var m = S.Medium; + if (!StressRetardation(ctx, w0, out double C, out double r0)) return; + r.Values.TryGetValue("heatW", out double heat); + double n0 = S.Host.IndexAt(w0), a = m.ThermalExpansionPerK, K = m.ThermalConductivity, area = Math.PI * r0 * r0; double D = StressDepolarization(C, wmode, r0); double F(double cs) => K * area / heat / (0.5 * m.DnDT + a * cs * n0 * n0 * n0 + a * r0 * (n0 - 1) / S.LengthM); r.Values["depolarizationFraction"] = D; r.Values["stressRetardationRad"] = C; r.Values["stressFocalLengthRadialM"] = F(m.StressCr); r.Values["stressFocalLengthTangentialM"] = F(m.StressCphi); - ctx.Note(string.Format(CultureInfo.InvariantCulture, "thermal stress: retardation {0:G4} rad at the rod edge, depolarised fraction {1:P2}, f_r = {2:G4} m, f_phi = {3:G4} m", C, D, F(m.StressCr), F(m.StressCphi))); + if (!rotate) r.Values["depolarizationFractionPattern"] = patternFraction; + ctx.Note(string.Format(CultureInfo.InvariantCulture, "thermal stress: retardation {0:G4} rad at the rod edge, depolarised fraction {1:P2}{2}, f_r = {3:G4} m, f_phi = {4:G4} m", C, D, + rotate ? " (lumped: the beam-averaged value, not the four-lobed pattern)" : string.Format(CultureInfo.InvariantCulture, " (beam average); the four-lobed pattern on the grid depolarises {0:P2}", patternFraction), F(m.StressCr), F(m.StressCphi))); + if (!rotate) return; // a rotation, so the energy is kept and y input depolarises as x does var ny = new Complex[ax.Length]; double tx = Math.Sqrt(1 - D), ty = Math.Sqrt(D); for (int i = 0; i < ax.Length; i++) { var x = ax[i]; ny[i] = ay[i] * tx + x * ty; ax[i] = x * tx - ay[i] * ty; } ay = ny; } + /// + /// The four-lobed stress-birefringence pattern: at every transverse point (x, y) the radial and tangential polarisations of a + /// uniformly heated rod differ by delta = C min(r, r0)^2 / r0^2 (the tangential one delayed for C > 0). With phi the polar angle the + /// Jones matrix is R(-phi) diag(1, e^{i delta}) R(phi): x -> x (cos^2 phi + sin^2 phi e^{i delta}), y = x sin phi cos phi (1 - e^{i delta}), + /// so an x-polarised beam leaks |E|^2 sin^2(2 phi) sin^2(delta / 2) into y. Fields in layout [m * n + i]; the map is applied in place to + /// ex and ey (y is added to whatever ey holds, through the same matrix). Energy is kept exactly (the matrix is unitary). + /// + internal static void ApplyStressPattern(Complex[] ex, Complex[] ey, int n, ITransverseBasis b, double C, double r0) + { + int M = b.M; + for (int m = 0; m < M; m++) + { + double x = b.X[m], y = b.Y[m], rr = Math.Sqrt(x * x + y * y); + double c2 = rr > 0 ? x * x / (rr * rr) : 1, s2 = 1 - c2, sc = rr > 0 ? x * y / (rr * rr) : 0; + double u = Math.Min(rr, r0) / r0, delta = C * u * u; + var e = Complex.FromPolarCoordinates(1, delta); + Complex txx = c2 + s2 * e, txy = sc * (1 - e), tyy = s2 + c2 * e; + int o = m * n; + for (int i = 0; i < n; i++) + { + var a = ex[o + i]; var bb = ey[o + i]; + ex[o + i] = txx * a + txy * bb; + ey[o + i] = txy * a + tyy * bb; + } + } + } + /// Depolarised fraction of a Gaussian beam (1/e^2 radius w) in a rod of radius r0 with edge retardation C: /// D = (1/2) <sin^2(delta/2)> over the beam inside the rod, delta = C r^2 / r0^2. A beam much wider than the rod gives /// Koechner's filled-rod (1 - sin C / C) / 4; a small beam gives (x^2 / (1 + x^2)) / 4, x = C w^2 / (2 r0^2). @@ -350,6 +422,7 @@ public PulseField Execute(ActiveContext ctx, PulseField p) if (S.Ase && basis == null) gc.Ase = new Random(S.AseSeed); else if (S.Ase) notes.Add("ASE is modelled in 1-D only (ignored on the spatial grid)"); var gd = gc.Prepare(g, S.Host); for (int i = 0; i < n; i++) gd.KRel[i] = phHost[i] / Math.Max(1e-30, S.LengthM); + if (S.AseModel.Enabled) gc.AseDepletion = new AseModel(gc, S.AseModel, S.Host.IndexAt(w0), w0); GainRun.Initialise(gc, S, w0); int K = S.Segments > 0 ? S.Segments : GainRun.AutoSegments(p); r.Values["segments"] = K; @@ -395,6 +468,7 @@ public PulseField Execute(ActiveContext ctx, PulseField p) notes.Add(rep > 0 ? "steady state disabled: single shot from the unpumped crystal" : "no repetition rate: single shot (" + (S.Pump.Kind == "cw" ? "cw pump to equilibrium" : "pump pulse " + (S.Pump.DelayS * 1e6).ToString("G3", CultureInfo.InvariantCulture) + " us before the seed") + ")"); } GainRun.RecordBefore(ctx, gc, S, p, w0); + GainRun.RecordAse(ctx, gc, S, Math.PI * wmode * wmode / 2, powerScale); // ------------------------------------------------ thermal lens ISpatialField thermal = null; double fth = double.PositiveInfinity; if (pumped) @@ -418,12 +492,13 @@ public PulseField Execute(ActiveContext ctx, PulseField p) if (!double.IsNaN(S.TrackedThermalFocalLengthM)) { r.Values["trackedThermalLensFocalLengthM"] = S.TrackedThermalFocalLengthM; + if (!double.IsNaN(S.TrackedEffectiveFocalLengthM)) r.Values["trackedThermalLensEffectiveFocalLengthM"] = S.TrackedEffectiveFocalLengthM; if (!double.IsNaN(S.TrackedOnAxisFocalLengthM) && !double.IsInfinity(fth) && Math.Abs(S.TrackedOnAxisFocalLengthM / fth - 1) > 0.05) notes.Add(string.Format(CultureInfo.InvariantCulture, "beam tracking estimated the thermal lens with the pump alone (on-axis f_th = {0:G4} m); with the pulse train it is f_th = {1:G4} m -- the tracked beam after this element uses the estimate (give thermalLens.focalLengthM to set it)", S.TrackedOnAxisFocalLengthM, fth)); } if (!double.IsNaN(S.ThermalFocalLengthM) && S.ThermalLens) fth = S.ThermalFocalLengthM; // ------------------------------------------------ amplification - Complex[] ax, ay = p.Ay; GainCrystal.PassStats st; TransverseProfile profOut = p.Profile; + Complex[] ax, ay = p.Ay; GainCrystal.PassStats st; TransverseProfile profOut = p.Profile; double stressPattern = double.NaN; double nonGainLoss = 0; if (!spatial) { @@ -449,8 +524,23 @@ public PulseField Execute(ActiveContext ctx, PulseField p) double after = Energy(E, basis, g); nonGainLoss = before + st.EnergyGainedJ - after; basis.ToSpace(E, n); - ax = TransverseOps.Collapse(E, basis, g.OmegaAbsolute, S.Id, out profOut); - if (p.Ay.Any(c => c != Complex.Zero)) notes.Add("spatial gain is scalar (x); the y component passed unchanged"); + bool yIn = p.Ay.Any(c => c != Complex.Zero); + if (S.Stress && basis is CartesianBasis cb && cb.Ny > 1 && !yIn && StressRetardation(ctx, w0, out double Cst, out double r0st)) + { + // the four-lobed pattern: x field in, x and y out through the per-point Jones map + var Ey = new Complex[E.Length]; + ApplyStressPattern(E, Ey, n, basis, Cst, r0st); + double ex2 = 0, ey2 = 0; + for (int qq = 0; qq < E.Length; qq++) { ex2 += E[qq].Real * E[qq].Real + E[qq].Imaginary * E[qq].Imaginary; ey2 += Ey[qq].Real * Ey[qq].Real + Ey[qq].Imaginary * Ey[qq].Imaginary; } + stressPattern = ey2 / Math.Max(1e-300, ex2 + ey2); + (ax, ay) = TransverseOps.CollapseVector(E, Ey, basis, g.OmegaAbsolute, S.Id, out profOut); + } + else + { + ax = TransverseOps.Collapse(E, basis, g.OmegaAbsolute, S.Id, out profOut); + if (yIn) notes.Add("spatial gain is scalar (x); the y component passed unchanged"); + if (S.Stress) notes.Add("stress birefringence: the four-lobed pattern needs a cartesian transverse grid (ny > 1) and an x-polarised input; the lumped depolarisation is used"); + } var flu = TransverseOps.Fluence(E, n, g, basis); r.Values["peakFluenceOutJPerM2"] = flu.Max(); r.Representation = basis.Describe() + " x " + n + " samples, " + S.Slices + " slices"; @@ -458,12 +548,13 @@ public PulseField Execute(ActiveContext ctx, PulseField p) } if (r.Representation == null) r.Representation = "1-D (mode area " + (gc.CellArea[0] * 1e6).ToString("G4", CultureInfo.InvariantCulture) + " mm^2), " + S.Slices + " slices, " + K + " time segments"; r.BIntegralRad = st.BIntegralRad; - if (S.Stress) ApplyStress(ctx, ax, ref ay, wmode, w0); + if (S.Stress) ApplyStress(ctx, ax, ref ay, wmode, w0, double.IsNaN(stressPattern), stressPattern); var q = new PulseField(g, ax, ay, rep > 0 ? rep : p.RepRateHz) { Profile = profOut }; double e1 = q.TotalEnergyJ(); GainRun.RecordAfter(ctx, gc, st.EnergyGainedJ); r.Values["gain"] = e1 / Math.Max(1e-300, e0); if (rep > 0) r.Values["averageOutputPowerW"] = e1 * rep; + GainRun.RecordAsePedestal(r, e1, rep); ctx.Energy("gain (from inversion)", -st.EnergyGainedJ); if (st.AseSeedJ != 0) { ctx.Energy("ASE (spontaneous emission)", -st.AseSeedJ); r.Values["aseSeedEnergyJ"] = st.AseSeedJ; } double rest0 = st.AseSeedJ; @@ -497,6 +588,8 @@ public sealed class AmplifierElement : IActiveElement, Engine.IReportsCarrierPha public int RoundTrips; // 0 = auto public int PassesPerRoundTrip = 2, MaxRoundTrips = 300; public PassOperation Between = new PassOperation(); + /// Thin-lens-equivalent and effective focal length (m) of the thermal lens put on the tracked beam, NaN if none. + public (double ThinM, double EffectiveM) TrackedLens = (double.NaN, double.NaN); public string Kind => "amplifier"; public string Description => (Geometry == "regenerative" ? "regenerative amplifier (" + (RoundTrips > 0 ? RoundTrips + " round trips" : "auto round trips") + ")" : "multipass amplifier (" + Passes + " passes)") + ", " + S.Describe(); public double OutputOmega0 => double.NaN; @@ -513,6 +606,7 @@ public PulseField Execute(ActiveContext ctx, PulseField p) Executor.Combine(new LinearTerm[] { bulk }, axis, out var phHost, out _, out _, out _); var gc = new GainCrystal(S.Medium, S.LengthM, S.Slices, S.Pump, wmode, null, S.Doping); if (S.Ase) gc.Ase = new Random(S.AseSeed); + if (S.AseModel.Enabled) gc.AseDepletion = new AseModel(gc, S.AseModel, S.Host.IndexAt(w0), w0); var gd = gc.Prepare(g, S.Host); for (int i = 0; i < n; i++) gd.KRel[i] = phHost[i] / Math.Max(1e-30, S.LengthM); GainRun.Initialise(gc, S, w0); @@ -540,6 +634,8 @@ public PulseField Execute(ActiveContext ctx, PulseField p) } else if (pumped) powerScale = GainRun.SingleShot(gc, S); GainRun.RecordBefore(ctx, gc, S, p, w0); + GainRun.RecordAse(ctx, gc, S, area, powerScale); + if (!double.IsNaN(TrackedLens.ThinM)) { r.Values["trackedThermalLensFocalLengthM"] = TrackedLens.ThinM; r.Values["trackedThermalLensEffectiveFocalLengthM"] = TrackedLens.EffectiveM; } if (pumped) { double absJ = gc.AbsorbedPumpJ.Sum(); r.Values["absorbedPumpEnergyJ"] = absJ; @@ -604,6 +700,7 @@ public PulseField Execute(ActiveContext ctx, PulseField p) var q = new PulseField(g, ax, ay, rep > 0 ? rep : p.RepRateHz) { Profile = p.Profile }; double e1 = q.TotalEnergyJ(); GainRun.RecordAfter(ctx, gc, gained); + GainRun.RecordAsePedestal(r, e1, rep); r.Values["gain"] = e1 / Math.Max(1e-300, e0); if (rep > 0) r.Values["averageOutputPowerW"] = e1 * rep; ctx.Energy("gain (from inversion)", -gained); @@ -653,7 +750,7 @@ public static void Compile(CompilerServices c, SystemElement e, CompiledElement s.Medium = GainMedium.FromJson(c.System.Materials.GetObject(mv.AsString()).GetObject("gain"), c.Registry, "materials." + mv.AsString() + ".gain"); else s.Medium = GainMedium.FromJson(mv, c.Registry, origin + ".medium"); if (j.Has("temperatureK")) s.Medium.TemperatureK = c.Num(e, "temperatureK", s.Medium.TemperatureK); - if (j.Has("dopingDensityM3")) s.Medium.DopingDensityM3 = c.Num(e, "dopingDensityM3", s.Medium.DopingDensityM3); + if (j.Has("dopingDensityM3")) { s.Medium.DopingDensityM3 = c.Num(e, "dopingDensityM3", s.Medium.DopingDensityM3); s.Medium.Used.Remove("doping"); } s.LengthM = c.Num(e, "lengthM", double.NaN); if (!(s.LengthM > 0)) throw new JsonException(origin + ": needs lengthM > 0"); ce.LengthM = s.LengthM; @@ -703,6 +800,7 @@ public static void Compile(CompilerServices c, SystemElement e, CompiledElement } s.SingleShotPumpS = c.Num(e, "pumpDurationS", double.NaN); s.Ase = j.GetBool("ase", false); s.AseSeed = j.GetInt("aseSeed", 7); + s.AseModel = AseSpec.FromJson(j["aseModel"]); if (j.Has("dopingProfile")) s.Doping = c.Registry.Create("field", ModelSpec.FromJson(j["dopingProfile"], ModelSource.SystemData, origin + ".dopingProfile")); s.Properties = c.SpatialProperties(e); if (!amplifier) @@ -734,17 +832,30 @@ public static void Compile(CompilerServices c, SystemElement e, CompiledElement s.Transverse = j.Has("transverse") ? TransverseSpec.FromJson(j["transverse"]) : null; ce.Active = a; var tlo = tlv != null && tlv.Kind == JsonKind.Object ? tlv.AsObject() : null; - if (a.Geometry == "multipass" && tlo != null && (tlo.Has("track") || tlo.Has("passGapM"))) + bool asked = tlo != null && (tlo.Has("track") || tlo.Has("passGapM")); + if (a.Geometry == "regenerative" && asked && a.RoundTrips < 1) + ce.Notes.Add("beam tracking: the thermal lens of a regenerative amplifier needs an explicit roundTrips (\"auto\" is decided at run time, after the beam is tracked)"); + else if (asked) { - // on request: the path between passes is only known if it is given + // on request: the path between passes (and, for a regenerative amplifier, the number of round trips) is only known if it is given ce.TrackingIndex = s.Host.IndexAt(c.Omega0); TrackThermalLens(c, ce, s); - ce.TrackPasses = a.Passes; ce.TrackPassGapM = tlo.GetDouble("passGapM", 0); + ce.TrackPasses = a.Geometry == "multipass" ? a.Passes : a.RoundTrips * a.PassesPerRoundTrip; ce.TrackPassGapM = tlo.GetDouble("passGapM", 0); + if (ce.TrackDucts != null && a.Geometry == "regenerative") + ce.Notes.Add(string.Format(CultureInfo.InvariantCulture, "beam tracking: {0} round trips x {1} passes through the thermal lens, {2:G4} m of free path between passes (an open path: the cavity's own focusing optics are not part of the tracked beam)", a.RoundTrips, a.PassesPerRoundTrip, ce.TrackPassGapM)); + a.TrackedLens = (s.TrackedThermalFocalLengthM, s.TrackedEffectiveFocalLengthM); } - else if (c.Tracked != null && s.ThermalLens && s.Pump.Kind != "none") ce.Notes.Add("beam tracking: the amplifier's thermal lens is not applied to the tracked beam (multipass: give thermalLens.track and passGapM)"); + else if (c.Tracked != null && s.ThermalLens && s.Pump.Kind != "none") ce.Notes.Add("beam tracking: the amplifier's thermal lens is not applied to the tracked beam (give thermalLens.track and passGapM; a regenerative amplifier also needs an explicit roundTrips)"); } c.Provenance.Record("element:" + e.Id, amplifier ? "amplifier" : "gain", ce.Active.Description, ModelSource.SystemData, "system file"); c.Provenance.Record("element:" + e.Id, "gainMedium", s.Medium.Describe() + (string.IsNullOrEmpty(s.Medium.References) ? "" : " [" + s.Medium.References + "]"), ModelSource.SystemData, "gain-medium library / system file"); + // the catalogue values the medium really uses: thermal data only with a thermal lens, stress constants only with stress birefringence + foreach (var q in s.Medium.Used.Values) + { + if (q.Name == "thermal" && !(s.ThermalLens && s.Pump.Kind != "none")) continue; + if (q.Name == "stress" && !s.Stress) continue; + c.Provenance.UseCatalogue("element:" + e.Id, q.Name, q, s.Medium.Name, c.Notes); + } } /// @@ -761,9 +872,14 @@ private static void TrackThermalLens(CompilerServices c, CompiledElement ce, Gai var ci = CultureInfo.InvariantCulture; if (!double.IsNaN(s.ThermalFocalLengthM)) { - ce.TrackLenses = new List<(double, double)> { (s.LengthM / 2, 1 / s.ThermalFocalLengthM) }; - s.TrackedThermalFocalLengthM = s.ThermalFocalLengthM; - ce.Notes.Add(string.Format(ci, "beam tracking: thermal lens f_th = {0:G4} m (thermalLens.focalLengthM) at the centre of the medium", s.ThermalFocalLengthM)); + // a measured focal length is what one sees from outside the medium: the effective focal length -1/C of the whole duct + double nT = ce.TrackingIndex > 0 ? ce.TrackingIndex : 1.0; + double thin = Optics.BeamState.ThinPowerForEffectiveFocalLength(s.LengthM, nT, s.ThermalFocalLengthM, out bool capped); + ce.TrackDucts = new List<(double, double, double)> { (0, s.LengthM, thin) }; + s.TrackedThermalFocalLengthM = 1 / thin; + s.TrackedEffectiveFocalLengthM = EffectiveFocalLength(ce.TrackDucts, ce.TrackingIndex); + ce.Notes.Add(string.Format(ci, "beam tracking: thermal lens with effective focal length {0:G4} m (thermalLens.focalLengthM) spread uniformly over the medium as a graded-index duct; thin-lens equivalent {1:G4} m", s.ThermalFocalLengthM, 1 / thin)); + if (capped) ce.Notes.Add("beam tracking: the given focal length is shorter than a quarter pitch of the medium can give (the beam would come to a focus inside it); the strongest duct with a monotone matrix is used"); return; } if (s.Pump.Kind == "none") return; @@ -785,9 +901,24 @@ private static void TrackThermalLens(CompilerServices c, CompiledElement ce, Gai var tl = gc.ThermalLens(scale, GainRun.HeatFraction(s, c.Omega0), s.RodRadiusM > 0 ? s.RodRadiusM : 3 * s.Pump.RadiusM, inv, wmode); double sum = inv.Sum(); if (double.IsInfinity(tl.FocalLengthM) || sum == 0) return; - ce.TrackLenses = Enumerable.Range(0, gc.Nz).Select(j => ((j + 0.5) * gc.Dz, inv[j])).ToList(); + ce.TrackDucts = Enumerable.Range(0, gc.Nz).Select(j => (j * gc.Dz, gc.Dz, inv[j])).ToList(); s.TrackedThermalFocalLengthM = 1 / sum; s.TrackedOnAxisFocalLengthM = tl.FocalLengthM; - ce.Notes.Add(string.Format(ci, "beam tracking: thermal lens f = {0:G4} m for the {1:G4} mm beam (on-axis f_th = {2:G4} m; {3:G4} W of heat, pump alone), applied as {4} thin lenses through the medium", 1 / sum, wmode * 1e3, tl.FocalLengthM, tl.HeatW, gc.Nz)); + s.TrackedEffectiveFocalLengthM = EffectiveFocalLength(ce.TrackDucts, ce.TrackingIndex); + ce.Notes.Add(string.Format(ci, "beam tracking: thermal lens f = {0:G4} m (thin-lens equivalent; effective {5:G4} m) for the {1:G4} mm beam (on-axis f_th = {2:G4} m; {3:G4} W of heat, pump alone), applied as {4} graded-index ducts through the medium", 1 / sum, wmode * 1e3, tl.FocalLengthM, tl.HeatW, gc.Nz, s.TrackedEffectiveFocalLengthM)); + } + + /// Effective focal length -1/C of the chain of ducts (one pass, reduced ABCD, output into vacuum). + internal static double EffectiveFocalLength(List<(double ZM, double LengthM, double InvFocalM)> ducts, double n) + { + if (!(n > 0)) n = 1; + var m = new[] { 1.0, 0, 0, 1 }; double z = 0; + foreach (var (z0, len, invF) in ducts.OrderBy(d => d.ZM)) + { + if (z0 > z) m = Optics.BeamState.Multiply(new[] { 1.0, (z0 - z) / n, 0, 1 }, m); + m = Optics.BeamState.Multiply(Optics.BeamState.DuctMatrix(len, n, invF / (n * len)), m); + z = z0 + len; + } + return m[2] != 0 ? -1 / m[2] : double.PositiveInfinity; } } } diff --git a/src/UltrafastEngine.Core/Gain/GainMedium.cs b/src/UltrafastEngine.Core/Gain/GainMedium.cs index 0c46a9a..580b4e1 100644 --- a/src/UltrafastEngine.Core/Gain/GainMedium.cs +++ b/src/UltrafastEngine.Core/Gain/GainMedium.cs @@ -26,8 +26,10 @@ public sealed class GainMedium public double TemperatureK { get; set; } = 293.15; /// Zero-phonon line (m) for the McCumber relation; NaN = four-level (no linking). public double ZeroLineM { get; set; } = double.NaN; - public double ThermalConductivity { get; set; } = 10; // W / (m K) - public double DnDT { get; set; } = 7.8e-6; // 1/K + public double ThermalConductivity { get; set; } = GenericThermal.Number("thermalConductivityWPerMK"); // W / (m K) + public double DnDT { get; set; } = GenericThermal.Number("dnDTPerK"); // 1/K + /// The generic thermal data (catalogue entry "defaults:gainMedium") of a user-defined medium that gives none. + public static CatalogueQuantity GenericThermal => Catalogue.Get("defaults:gainMedium", "default").Q("thermal"); /// Stress birefringence of a cubic host (Koechner): thermal expansion (1/K) and photo-elastic coefficients C_r, C_phi; NaN = not available. public double ThermalExpansionPerK { get; set; } = double.NaN; public double StressCr { get; set; } = double.NaN; @@ -81,7 +83,7 @@ public string Describe() => string.Format(CultureInfo.InvariantCulture, "{0} (ho // ------------------------------------------------------------------------------------------ library - public static readonly string[] Names = { "Yb:YAG", "Ti:sapphire", "Nd:YAG" }; + public static string[] Names => Catalogue.Entries("gain").Select(e => e.Id).ToArray(); /// Sum of Gaussian bands in wavelength: (lambda0 m, peak m^2, FWHM m). internal static Func Bands(params (double L0, double S, double Fwhm)[] b) @@ -91,60 +93,44 @@ internal static Func Bands(params (double L0, double S, double F internal static Func Asym(double l0, double s, double hwhmShort, double hwhmLong) => w => { double l = 2 * Math.PI * C / w, h = l < l0 ? hwhmShort : hwhmLong, u = (l - l0) / h; return s * Math.Exp(-Math.Log(2) * u * u); }; + /// The catalogue quantities this medium takes its numbers from ("doping", "lifetime", "thermal", "emission", ...); a user override removes the quantity. + public Dictionary Used { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// A built-in gain medium from the catalogue (Data/materials/gain): lifetime, doping, thermal and stress data, cross-section bands, each with its source and status. public static GainMedium Get(string name, double temperatureK = double.NaN) { string k = (name ?? "").Trim().ToLowerInvariant().Replace(" ", "").Replace("-", ""); - const double cm2 = 1e-4; // cm^2 -> m^2 - GainMedium g; - switch (k) + var e = Catalogue.Find(k, "gain") ?? throw new JsonException("unknown gain medium \"" + name + "\" (built-in: " + string.Join(", ", Names) + "); define your own with an object (docs/reference/gain.md)"); + var dop = e.Q("doping"); var th = e.Q("thermal"); var em = e.Q("emission"); var ab = e.Q("absorption"); + var g = new GainMedium { - case "yb:yag": case "ybyag": - g = new GainMedium - { - Name = "Yb:YAG", Host = "YAG", LifetimeS = 0.95e-3, DopingDensityM3 = 1.38e26 * 2, ZeroLineM = 968.8e-9, - ThermalConductivity = 9.5, DnDT = 7.8e-6, ThermalExpansionPerK = 7.5e-6, StressCr = 0.017, StressCphi = -0.0025, - // emission in the lasing band (room temperature): main line 1030 nm, shoulder ~1048 nm, broad base - EmissionBand = Bands((1030.0e-9, 2.1e-20 * cm2, 8.5e-9), (1048.5e-9, 0.32e-20 * cm2, 16e-9), (1015e-9, 0.35e-20 * cm2, 30e-9)), - // absorption in the pump band: 940 nm (broad) and the 969 nm zero-phonon line - AbsorptionBand = Bands((940.5e-9, 0.77e-20 * cm2, 18e-9), (968.8e-9, 0.85e-20 * cm2, 2.8e-9), (915e-9, 0.25e-20 * cm2, 15e-9)), - EmissionDescription = "Gaussian-band fit: 2.1e-20 cm^2 at 1030 nm (8.5 nm FWHM), 1048 nm shoulder, broad base", - AbsorptionDescription = "Gaussian-band fit: 0.77e-20 cm^2 at 940 nm (18 nm FWHM), 0.85e-20 cm^2 at 969 nm (2.8 nm)", - References = "room-temperature values after Koerner et al., Appl. Phys. B 75, 2002 (cross sections) and Sumida & Fan, OSA TOPS 1995 (lifetime 0.95 ms); lasing-band absorption and pump-band emission from the McCumber relation (zero line 968.8 nm); approximate analytic fit (~10-20 %) -- override with measured tables" - }; - g.Notes.Add("Yb:YAG cross sections are an analytic room-temperature fit; the McCumber relation gives reabsorption at the lasing wavelength (~0.1e-20 cm^2 at 1030 nm). Cryogenic linewidths are not modelled (only the Boltzmann factor follows temperatureK)"); - break; - case "ti:sapphire": case "ti:sa": case "tisa": case "tisapphire": case "ti:al2o3": - g = new GainMedium - { - Name = "Ti:sapphire", Host = "Sapphire", LifetimeS = 3.2e-6, DopingDensityM3 = 3.3e25, - ThermalConductivity = 33, DnDT = 13e-6, - EmissionBand = Asym(795e-9, 3.0e-19 * cm2, 75e-9, 140e-9), - AbsorptionBand = Bands((490e-9, 6.4e-20 * cm2, 115e-9)), - EmissionDescription = "asymmetric-Gaussian fit, pi polarisation: 3.0e-19 cm^2 at 795 nm, half widths 75 / 140 nm", - AbsorptionDescription = "Gaussian fit, pi polarisation: 6.4e-20 cm^2 at 490 nm, 115 nm FWHM (~4.4e-20 cm^2 at 532 nm)", - References = "after Moulton, JOSA B 3, 125 (1986) (pi-polarised cross sections, lifetime 3.2 us); four-level; approximate analytic fit (~10-20 %) -- override with measured tables" - }; - g.Notes.Add("Ti:sapphire is treated as a four-level system (no lasing-band absorption); the residual infrared absorption (figure of merit) is not included -- add it with absorptionProfile or a material attenuation"); - break; - case "nd:yag": case "ndyag": - g = new GainMedium - { - Name = "Nd:YAG", Host = "YAG", LifetimeS = 230e-6, DopingDensityM3 = 1.38e26, - ThermalConductivity = 10.5, DnDT = 7.8e-6, ThermalExpansionPerK = 7.5e-6, StressCr = 0.017, StressCphi = -0.0025, - EmissionBand = Bands((1064.2e-9, 2.8e-19 * cm2, 0.6e-9)), - AbsorptionBand = Bands((808.5e-9, 7.0e-20 * cm2, 2.5e-9)), - EmissionDescription = "Gaussian fit: 2.8e-19 cm^2 at 1064.2 nm, 0.6 nm FWHM", - AbsorptionDescription = "Gaussian fit: 7e-20 cm^2 at 808.5 nm, 2.5 nm FWHM", - References = "Koechner, Solid-State Laser Engineering (1 at.% Nd, tau = 230 us); four-level; approximate analytic fit" - }; - break; - default: - throw new JsonException("unknown gain medium \"" + name + "\" (built-in: " + string.Join(", ", Names) + "); define your own with an object (docs/reference/gain.md)"); - } + Name = e.Id, Host = dop.Text("host"), LifetimeS = e.Q("lifetime").Number("lifetimeS"), DopingDensityM3 = dop.Number("dopingDensityM3"), + ThermalConductivity = th.Number("thermalConductivityWPerMK"), DnDT = th.Number("dnDTPerK"), + EmissionBand = Band(em), AbsorptionBand = Band(ab), EmissionDescription = em.Text("description", ""), AbsorptionDescription = ab.Text("description", ""), + References = em.Source + }; + foreach (var q in e.Quantities.Values) g.Used[q.Name] = q; + var zl = e.TryQ("zeroLine"); if (zl != null) g.ZeroLineM = zl.Number("zeroLineWavelengthM"); + var st = e.TryQ("stress"); if (st != null) { g.ThermalExpansionPerK = st.Number("thermalExpansionPerK"); g.StressCr = st.Number("stressCr"); g.StressCphi = st.Number("stressCphi"); } + if (!string.IsNullOrEmpty(e.Notes)) g.Notes.Add(e.Notes); if (!double.IsNaN(temperatureK)) g.TemperatureK = temperatureK; return g; } + /// A cross-section band set of a catalogue quantity: "gaussians" (bands in wavelength) or "asymmetricGaussian"; cross sections are stored in cm^2. + private static Func Band(CatalogueQuantity q) + { + const double cm2 = 1e-4; // cm^2 -> m^2 + switch (q.Text("shape", "")) + { + case "gaussians": + return Bands(q.Json.GetArray("bands").Select(x => x.AsObject()).Select(x => (x.GetDouble("wavelengthM", 0), x.GetDouble("sigmaCm2", 0) * cm2, x.GetDouble("fwhmM", 1e-9))).ToArray()); + case "asymmetricGaussian": + return Asym(q.Number("peakWavelengthM"), q.Number("sigmaCm2") * cm2, q.Number("hwhmShortM"), q.Number("hwhmLongM")); + default: throw new JsonException("catalogue " + q.Path + ": unknown cross-section shape \"" + q.Text("shape", "") + "\""); + } + } + /// /// A gain medium from JSON: {"base": "Yb:YAG", ...overrides} or a full definition with "emission" / "absorption" /// cross-section models ({"kind": "gaussians", "bands": [{"wavelengthM", "sigmaM2", "fwhmM"}]}, {"kind": "table", @@ -157,20 +143,23 @@ public static GainMedium FromJson(JsonValue v, ModelRegistry reg, string origin) if (v.Kind == JsonKind.String) return Get(v.AsString()); var o = v.AsObject(); var g = o.Has("base") ? Get(o.GetString("base")) : new GainMedium { Name = o.GetString("name", "user gain medium"), References = "user definition" }; + if (!o.Has("base")) g.Used["thermal"] = GenericThermal; // reported in the run notes unless both values are given below if (o.Has("name")) g.Name = o.GetString("name"); if (o.Has("host")) g.Host = o.GetString("host"); - if (o.Has("lifetimeS")) g.LifetimeS = o.GetDouble("lifetimeS", g.LifetimeS); - if (o.Has("dopingDensityM3")) g.DopingDensityM3 = o.GetDouble("dopingDensityM3", g.DopingDensityM3); - if (o.Has("dopingAtPercent")) g.DopingDensityM3 = o.GetDouble("dopingAtPercent", 1) / 100 * o.GetDouble("sitesPerM3", 1.38e28); + if (o.Has("lifetimeS")) { g.LifetimeS = o.GetDouble("lifetimeS", g.LifetimeS); g.Used.Remove("lifetime"); } + if (o.Has("dopingDensityM3")) { g.DopingDensityM3 = o.GetDouble("dopingDensityM3", g.DopingDensityM3); g.Used.Remove("doping"); } + if (o.Has("dopingAtPercent")) { g.DopingDensityM3 = o.GetDouble("dopingAtPercent", 1) / 100 * o.GetDouble("sitesPerM3", 1.38e28); g.Used.Remove("doping"); } if (o.Has("temperatureK")) g.TemperatureK = o.GetDouble("temperatureK", g.TemperatureK); - if (o.Has("zeroLineWavelengthM")) g.ZeroLineM = o.GetDouble("zeroLineWavelengthM", double.NaN); + if (o.Has("zeroLineWavelengthM")) { g.ZeroLineM = o.GetDouble("zeroLineWavelengthM", double.NaN); g.Used.Remove("zeroLine"); } if (o.Has("thermalConductivity")) g.ThermalConductivity = o.GetDouble("thermalConductivity", g.ThermalConductivity); if (o.Has("dnDT")) g.DnDT = o.GetDouble("dnDT", g.DnDT); + if (o.Has("thermalConductivity") && o.Has("dnDT")) g.Used.Remove("thermal"); // a partial override still uses one default + if (o.Has("thermalExpansionPerK") || o.Has("stressCr") || o.Has("stressCphi")) g.Used.Remove("stress"); g.ThermalExpansionPerK = o.GetDouble("thermalExpansionPerK", g.ThermalExpansionPerK); g.StressCr = o.GetDouble("stressCr", g.StressCr); g.StressCphi = o.GetDouble("stressCphi", g.StressCphi); if (o.Has("references")) g.References = o.GetString("references"); - if (o.Has("emission")) { g.EmissionBand = CrossSection(o["emission"], reg, origin + ".emission", out var d); g.EmissionDescription = d; } - if (o.Has("absorption")) { g.AbsorptionBand = CrossSection(o["absorption"], reg, origin + ".absorption", out var d); g.AbsorptionDescription = d; } + if (o.Has("emission")) { g.Used.Remove("emission"); g.EmissionBand = CrossSection(o["emission"], reg, origin + ".emission", out var d); g.EmissionDescription = d; } + if (o.Has("absorption")) { g.Used.Remove("absorption"); g.AbsorptionBand = CrossSection(o["absorption"], reg, origin + ".absorption", out var d); g.AbsorptionDescription = d; } if (!(g.LifetimeS > 0)) throw new JsonException(origin + ": gain medium needs lifetimeS > 0"); if (!(g.DopingDensityM3 > 0)) throw new JsonException(origin + ": gain medium needs dopingDensityM3 > 0"); return g; diff --git a/src/UltrafastEngine.Core/Materials/Catalogue.cs b/src/UltrafastEngine.Core/Materials/Catalogue.cs new file mode 100644 index 0000000..1c3af78 --- /dev/null +++ b/src/UltrafastEngine.Core/Materials/Catalogue.cs @@ -0,0 +1,243 @@ +// Catalogue.cs -- the material catalogue: every material number of the engine, with its provenance. +// +// What it does: loads the catalogue files (JSON with comments) that are embedded in the assembly under Data/materials/{solids,gases,raman,gain,crystals}/ +// and, on top of them, the user's own files (folder $ULTRAFAST_MATERIALS, default ~/.ultrafast/materials; a file replaces the quantities +// it names, with a notice). MaterialLibrary, GasData and the Raman models read their numbers from here; nothing is hard-coded in C#. +// +// Classes: +// CatalogueEntry one material, gas, Raman model, gain medium or chi(2) crystal: Id, Kind ("solid" | "gas" | "raman" | "gain" | "crystal"), Aliases, ListOrder, Notes, Quantities. +// CatalogueQuantity one quantity of an entry ("index", "n2", "rotation", ...): its numbers (Json), Source, Status, uncertainty, Origin. +// CatalogueStatus Verified (the number was read from the source), Approximate (typical / fitted), Unverified (not checked). +// Catalogue static: Find(nameOrAlias), Get, Entries(kind), Notices (user overrides), Problems() (lint), Reload(userFolder) for tests. +// +// File format: docs/reference/materials-catalogue.md (generated by `ultrafast reference materials`) and Data/materials/README.md. +// Units are in the key names (valueM2PerW, tau1S, ionizationEv, rotationalConstantBPerCm); coefficients keep the units of their source. +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using UltrafastEngine.Models; +using UltrafastEngine.Serialization; + +namespace UltrafastEngine.Materials +{ + /// How far a catalogue number has been checked against its source. + public enum CatalogueStatus + { + /// Read from the cited source and compared (the check is recorded in the entry). + Verified = 0, + /// A typical, rounded or fitted value; good to the stated uncertainty, not traced to one table. + Approximate = 1, + /// Not checked against any source yet (quoted from memory or from an earlier version of the code). + Unverified = 2 + } + + /// One quantity of a catalogue entry: the numbers and where they come from. + public sealed class CatalogueQuantity + { + public string EntryId { get; internal set; } + public string Name { get; internal set; } + /// The quantity's JSON object (numbers with units in their key names, plus the metadata keys). + public JsonObject Json { get; internal set; } + public string Source => Json.GetString("source", ""); + public CatalogueStatus Status { get; internal set; } + /// Relative uncertainty (1-sigma-like spread of the literature), NaN when not stated. + public double RelativeUncertainty => Json.GetDouble("relativeUncertainty", double.NaN); + public string Uncertainty => Json.GetString("uncertainty", ""); + /// "built-in" or "user: path". + public string Origin { get; internal set; } = "built-in"; + public bool IsUser => Origin.StartsWith("user", StringComparison.Ordinal); + public string Path => EntryId + "." + Name; + + public bool Has(string key) => Json.Has(key); + public double Number(string key) => Json.GetRequiredDouble(key, Path); + public double Number(string key, double fallback) => Json.GetDouble(key, fallback); + public double[] Array(string key) => Json.GetDoubleArray(key) ?? throw new ModelException("catalogue " + Path + ": missing array \"" + key + "\"."); + public string Text(string key, string fallback = null) => Json.GetString(key, fallback); + + /// One line for notes and reports: status, source, uncertainty. + public string Citation() + { + string s = Status.ToString().ToUpperInvariant() + ": " + Source; + if (!double.IsNaN(RelativeUncertainty)) s += string.Format(CultureInfo.InvariantCulture, " (uncertainty about {0:G2} %)", 100 * RelativeUncertainty); + else if (Uncertainty.Length > 0) s += " (" + Uncertainty + ")"; + if (IsUser) s += " [" + Origin + "]"; + return s; + } + } + + /// One material, gas or Raman model of the catalogue. + public sealed class CatalogueEntry + { + public string Id { get; internal set; } + public string Kind { get; internal set; } + public int ListOrder { get; internal set; } + public string Description { get; internal set; } + public string Notes { get; internal set; } + public List Aliases { get; } = new List(); + public Dictionary Quantities { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public string File { get; internal set; } + /// The entry's file as parsed: entry-level fields that are not quantities (displayName, opticalClass, references, ...). + public JsonObject Raw { get; internal set; } + + public bool Has(string quantity) => Quantities.ContainsKey(quantity); + public CatalogueQuantity Q(string quantity) => + Quantities.TryGetValue(quantity, out var q) ? q : throw new ModelException("catalogue entry '" + Id + "' has no quantity \"" + quantity + "\" (" + File + ")."); + public CatalogueQuantity TryQ(string quantity) => Quantities.TryGetValue(quantity, out var q) ? q : null; + } + + /// The material catalogue (see the file header). Thread-safe; loaded on first use. + public static class Catalogue + { + private const string Prefix = "UltrafastEngine.Data.materials."; + private static readonly string[] Kinds = { "solid", "gas", "raman", "gain", "crystal", "default" }; + + private sealed class State + { + public readonly Dictionary ById = new Dictionary(StringComparer.OrdinalIgnoreCase); + public readonly Dictionary Alias = new Dictionary(StringComparer.OrdinalIgnoreCase); + public readonly List Notices = new List(); + public readonly List Problems = new List(); + public readonly Dictionary Cache = new Dictionary(); + } + + private static readonly object Gate = new object(); + private static State _state; + private static string _userFolderOverride; // tests + + private static State S { get { lock (Gate) return _state ?? (_state = Load(UserFolder)); } } + + /// The folder with the user's own catalogue files ($ULTRAFAST_MATERIALS, else ~/.ultrafast/materials); may not exist. + public static string UserFolder + { + get + { + if (_userFolderOverride != null) return _userFolderOverride; + var env = Environment.GetEnvironmentVariable("ULTRAFAST_MATERIALS"); + if (env != null) return env; // an empty value switches the user catalogue off + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return string.IsNullOrEmpty(home) ? "" : System.IO.Path.Combine(home, ".ultrafast", "materials"); + } + } + + /// Notices about user files that replaced built-in quantities (shown in run notes by the compiler). + public static IReadOnlyList Notices => S.Notices; + + /// Format problems found while loading (empty for the shipped catalogue; the lint test asserts it). + public static IReadOnlyList Problems => S.Problems; + + /// Derived tables (GasData, index coefficients, ...) are built once per loaded catalogue: runs the first time for a key and the result is kept until the catalogue is reloaded. + public static T Cached(string key, Func build) where T : class + { + var s = S; + lock (s.Cache) + { + if (!s.Cache.TryGetValue(key, out var v)) s.Cache[key] = v = build(); + return (T)v; + } + } + + /// Reload with another user folder (null: the default). For tests. + internal static void Reload(string userFolder) + { + lock (Gate) { _userFolderOverride = userFolder; _state = null; } + } + + public static IEnumerable Entries(string kind = null) => + S.ById.Values.Where(e => kind == null || e.Kind == kind).OrderBy(e => e.ListOrder).ThenBy(e => e.Id, StringComparer.Ordinal); + + /// The entry for a name or alias (case-insensitive), or null. + public static CatalogueEntry Find(string nameOrAlias) + { + var s = S; + string n = (nameOrAlias ?? "").Trim(); + if (s.ById.TryGetValue(n, out var e)) return e; + if (s.Alias.TryGetValue(n, out var id) && s.ById.TryGetValue(id, out e)) return e; + return null; + } + + public static CatalogueEntry Find(string nameOrAlias, string kind) + { + var e = Find(nameOrAlias); + return e != null && e.Kind == kind ? e : null; + } + + public static CatalogueEntry Get(string id, string kind) => + Find(id, kind) ?? throw new ModelException("catalogue: no " + kind + " entry '" + id + "'."); + + // ------------------------------------------------------------------------------------------------ loading + + private static State Load(string userFolder) + { + var st = new State(); + var asm = typeof(Catalogue).Assembly; + foreach (var res in asm.GetManifestResourceNames().Where(r => r.StartsWith(Prefix, StringComparison.Ordinal) && r.EndsWith(".json", StringComparison.Ordinal)).OrderBy(r => r, StringComparer.Ordinal)) + { + string text; + using (var rs = asm.GetManifestResourceStream(res)) + using (var sr = new StreamReader(rs)) text = sr.ReadToEnd(); + Add(st, text, "built-in", res.Substring(Prefix.Length), user: false); + } + if (!string.IsNullOrEmpty(userFolder) && Directory.Exists(userFolder)) + foreach (var f in Directory.GetFiles(userFolder, "*.json", SearchOption.AllDirectories).OrderBy(x => x, StringComparer.Ordinal)) + Add(st, File.ReadAllText(f), "user: " + f, f, user: true); + return st; + } + + private static void Add(State st, string text, string origin, string file, bool user) + { + JsonObject o; + try { o = JsonValue.Parse(text).AsObject(); } + catch (JsonException ex) { throw new ModelException("catalogue file " + file + ": " + ex.Message); } + string id = o.GetString("id") ?? throw new ModelException("catalogue file " + file + ": missing \"id\"."); + st.ById.TryGetValue(id, out var existing); + string kind = o.GetString("kind") ?? existing?.Kind; + if (kind == null || !Kinds.Contains(kind)) throw new ModelException("catalogue file " + file + ": \"kind\" must be solid, gas, raman, gain, crystal or default."); + if (existing != null && existing.Kind != kind) throw new ModelException("catalogue file " + file + ": '" + id + "' is a " + existing.Kind + " in the built-in catalogue."); + var e = existing ?? new CatalogueEntry { Id = id, Kind = kind, File = file }; + if (existing == null) { st.ById[id] = e; e.Raw = o; } + else foreach (var kv in o) if (kv.Key != "quantities") e.Raw.Set(kv.Key, kv.Value); + if (o.Has("listOrder")) e.ListOrder = o.GetInt("listOrder", 1000); + else if (existing == null) e.ListOrder = 1000; + if (o.Has("description")) e.Description = o.GetString("description"); + if (o.Has("notes")) e.Notes = o.GetString("notes"); + if (o.Has("aliases")) + foreach (var a in o.GetArray("aliases")) + { + string al = a.AsString(); + if (!e.Aliases.Contains(al)) e.Aliases.Add(al); + st.Alias[al] = id; + } + var qs = o.GetObject("quantities"); + if (qs == null) { st.Problems.Add(file + ": no \"quantities\""); return; } + foreach (var kv in qs) + { + var qo = kv.Value.AsObject(); + var q = new CatalogueQuantity { EntryId = id, Name = kv.Key, Json = qo, Origin = origin }; + string status = qo.GetString("status", qo.Has("ref") ? "verified" : ""); // a "ref" quantity points at another entry, whose own quantities carry the status + switch (status.ToLowerInvariant()) + { + case "verified": q.Status = CatalogueStatus.Verified; break; + case "approximate": q.Status = CatalogueStatus.Approximate; break; + case "unverified": q.Status = CatalogueStatus.Unverified; break; + default: q.Status = CatalogueStatus.Unverified; st.Problems.Add(file + ": " + id + "." + kv.Key + " has status \"" + status + "\" (verified | approximate | unverified)"); break; + } + if (!qo.Has("ref") && qo.GetString("source", "").Trim().Length == 0) st.Problems.Add(file + ": " + id + "." + kv.Key + " has no \"source\""); + if (user && e.Quantities.ContainsKey(kv.Key)) + st.Notices.Add("user catalogue " + file + " replaces " + id + "." + kv.Key + " (" + e.Quantities[kv.Key].Status.ToString().ToLowerInvariant() + " built-in value)"); + e.Quantities[kv.Key] = q; + } + } + + /// The embedded catalogue files' text, by relative path (for the generated reference page and the lint test). + internal static IEnumerable> EmbeddedFiles() + { + var asm = typeof(Catalogue).Assembly; + foreach (var res in asm.GetManifestResourceNames().Where(r => r.StartsWith(Prefix, StringComparison.Ordinal) && r.EndsWith(".json", StringComparison.Ordinal)).OrderBy(r => r, StringComparer.Ordinal)) + using (var rs = asm.GetManifestResourceStream(res)) + using (var sr = new StreamReader(rs)) yield return new KeyValuePair(res.Substring(Prefix.Length), sr.ReadToEnd()); + } + } +} diff --git a/src/UltrafastEngine.Core/Materials/CatalogueModels.cs b/src/UltrafastEngine.Core/Materials/CatalogueModels.cs new file mode 100644 index 0000000..e9a2ec6 --- /dev/null +++ b/src/UltrafastEngine.Core/Materials/CatalogueModels.cs @@ -0,0 +1,97 @@ +// CatalogueModels.cs -- turns catalogue quantities into engine objects. +// +// What it does: builds IRefractiveIndex objects (constant, sellmeier, vendorSellmeier, cauchy) and delayed (Raman) responses from the +// numbers of a CatalogueEntry, and holds the defaults of the named Raman models ("silica" / "blowWood", "linAgrawal", "hollenbeck", +// "orientational") that the model registry uses when a run spec gives only the kind. +// The arithmetic is exactly what the old C# tables did (squared resonance wavelengths, A - S / C for the vendor form), so the move of the +// numbers into files changed no result (CatalogueSnapshotTests). +// +// CatalogueModels.Index(q) -> IRefractiveIndex (relative to the medium named by q's "referenceMedium") +// CatalogueModels.IsAirReferenced(q) -> true when the data are relative to air +// CatalogueModels.Raman(entry) -> the delayed response of a "raman" entry (with its anisotropic split) +// CatalogueModels.RamanFractionOf(entry) -> its default f_R (NaN when none) +using System; +using System.Linq; +using UltrafastEngine.Models; + +namespace UltrafastEngine.Materials +{ + public static class CatalogueModels + { + public static bool IsAirReferenced(CatalogueQuantity q) => string.Equals(q.Text("referenceMedium", "vacuum"), "air", StringComparison.OrdinalIgnoreCase); + + /// The (relative) index model of an "index" quantity of a solid. + public static IRefractiveIndex Index(CatalogueQuantity q) + { + string model = q.Text("model", ""); + switch (model) + { + case "constant": + return new ConstantIndex(q.Number("value")); + case "sellmeier": + { + var b = q.Array("b"); + double[] c = q.Has("poleUm2") ? q.Array("poleUm2") : q.Array("resonanceWavelengthsUm").Select(x => x * x).ToArray(); + return new SellmeierIndex(b, c, q.Number("a", 1.0), q.Text("name")); + } + case "vendorSellmeier": + { + // n^2 = a + s / (l^2 - c) - d l^2, written in the form of SellmeierIrIndex + double a = q.Number("a"), s = q.Number("numeratorUm2"), c = q.Number("poleUm2"), d = q.Number("irTermPerUm2"); + return new SellmeierIrIndex(a - s / c, new[] { s / c }, new[] { c }, d, q.Text("name")); + } + case "cauchy": + return new CauchyIndex(q.Array("coefficients")); + default: + throw new ModelException("catalogue " + q.Path + ": unknown index model \"" + model + "\" (constant, sellmeier, vendorSellmeier, cauchy)."); + } + } + + // ------------------------------------------------------------------------------------------------ Raman + + /// The anisotropic share to apply to a response: the entry's "anisotropicFraction" quantity, or 0. + private static double Aniso(CatalogueEntry e) => e.TryQ("anisotropicFraction")?.Number("value") ?? 0.0; + + public static IDelayedResponse Raman(CatalogueEntry e) + { + var r = e.Q("response"); + switch (r.Text("model", "")) + { + case "dampedOscillator": return Split(new DampedOscillatorResponse(r.Number("tau1S"), r.Number("tau2S")), Aniso(e)); + case "hollenbeckCantrell": return Split(new HollenbeckCantrellRamanResponse(), Aniso(e)); + case "linAgrawal": return new LinAgrawalRamanResponse(); + case "orientational": return new OrientationalResponse(r.Number("riseS"), r.Number("decayS"), r.Number("anisotropicFraction", 1.5)); + default: throw new ModelException("catalogue " + r.Path + ": unknown Raman model \"" + r.Text("model", "") + "\"."); + } + } + + private static IDelayedResponse Split(IDelayedResponse h, double b) => b != 0 ? new AnisotropicSplitResponse(h, b) : h; + + public static double RamanFractionOf(CatalogueEntry e) => e.TryQ("ramanFraction")?.Number("value") ?? double.NaN; + + /// Catalogue entry that holds the defaults of a registry kind ("silica" and "blowWood" are the same model). + public static CatalogueEntry RamanEntryForKind(string kind) + { + switch ((kind ?? "").ToLowerInvariant()) + { + case "silica": case "blowwood": return Catalogue.Get("blowWood", "raman"); + case "linagrawal": return Catalogue.Get("linAgrawal", "raman"); + case "hollenbeck": return Catalogue.Get("hollenbeck", "raman"); + case "orientational": return Catalogue.Get("orientational", "raman"); + default: return null; + } + } + + /// The defaults of the model registry's Raman kinds, from the catalogue (tau1, tau2, anisotropic fraction, ...). + public static class RamanDefaults + { + public static double BlowWoodTau1S => Catalogue.Get("blowWood", "raman").Q("response").Number("tau1S"); + public static double BlowWoodTau2S => Catalogue.Get("blowWood", "raman").Q("response").Number("tau2S"); + public static double BlowWoodAnisotropicFraction => Aniso(Catalogue.Get("blowWood", "raman")); + public static double HollenbeckAnisotropicFraction => Aniso(Catalogue.Get("hollenbeck", "raman")); + public static double OrientationalRiseS => Catalogue.Get("orientational", "raman").Q("response").Number("riseS"); + public static double OrientationalDecayS => Catalogue.Get("orientational", "raman").Q("response").Number("decayS"); + public static double OrientationalAnisotropicFraction => Catalogue.Get("orientational", "raman").Q("response").Number("anisotropicFraction"); + } + } +} diff --git a/src/UltrafastEngine.Core/Materials/CatalogueReference.cs b/src/UltrafastEngine.Core/Materials/CatalogueReference.cs new file mode 100644 index 0000000..a647af6 --- /dev/null +++ b/src/UltrafastEngine.Core/Materials/CatalogueReference.cs @@ -0,0 +1,125 @@ +// CatalogueReference.cs -- generates docs/reference/materials-catalogue.md from the shipped catalogue files. +// +// `ultrafast reference materials -o docs/reference/materials-catalogue.md` (part of /regen). It reads only the embedded files, never the +// user's own, so the page is the same on every machine. Layout: how to read it, counts by status, one table per material / gas / Raman model +// (quantity, values with their units, status, uncertainty, source), and the list of everything that is not verified yet. +// +// CatalogueReference.Markdown() -> the page text +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using UltrafastEngine.Serialization; + +namespace UltrafastEngine.Materials +{ + public static class CatalogueReference + { + private static readonly HashSet Meta = new HashSet { "source", "status", "uncertainty", "relativeUncertainty", "verification", "conditions", "name", "ref" }; + +#if NET + private static string Num(double v) => v.ToString("R", CultureInfo.InvariantCulture); +#else + // .NET Framework's "R" can print 17 digits for a value that 15 reproduce (56.306399999999996): use the shortest round-trip, as .NET does. + private static string Num(double v) + { + foreach (var f in new[] { "G15", "G16", "G17" }) + { + var t = v.ToString(f, CultureInfo.InvariantCulture); + if (double.Parse(t, CultureInfo.InvariantCulture) == v) return t; + } + return v.ToString("G17", CultureInfo.InvariantCulture); + } +#endif + + private static string Value(JsonValue v) + { + switch (v.Kind) + { + case JsonKind.Number: return Num(v.AsDouble()); + case JsonKind.String: return v.AsString(); + case JsonKind.Array: + var a = v.AsArray().ToList(); + return a.Count > 6 ? "[" + string.Join(", ", a.Take(3).Select(Value)) + ", ... (" + a.Count + " values)]" : "[" + string.Join(", ", a.Select(Value)) + "]"; + case JsonKind.Bool: return v.AsBool() ? "true" : "false"; + default: return v.ToJson(false); + } + } + + private static string Cell(string s) => (s ?? "").Replace("|", "\\|").Replace("\n", " "); + + private sealed class Row { public string Entry, Kind, Quantity, Values, Status, Uncertainty, Source, Check; } + + public static string Markdown() + { + var rows = new List(); + var entries = new List<(string File, JsonObject O)>(); + foreach (var f in Catalogue.EmbeddedFiles()) entries.Add((f.Key, JsonValue.Parse(f.Value).AsObject())); + foreach (var (file, o) in entries.OrderBy(x => x.O.GetInt("listOrder", 1000)).ThenBy(x => x.O.GetString("id"), StringComparer.Ordinal)) + foreach (var q in o.GetObject("quantities")) + { + var qo = q.Value.AsObject(); + var vals = qo.Where(kv => !Meta.Contains(kv.Key)).Select(kv => kv.Key + " = " + Value(kv.Value)); + var cond = qo.GetObject("conditions"); + string unc = !qo.Has("relativeUncertainty") ? qo.GetString("uncertainty", "") : "about " + Num(100 * qo.GetDouble("relativeUncertainty", 0)) + " %" + (qo.Has("uncertainty") ? "; " + qo.GetString("uncertainty") : ""); + string conditions = cond != null && cond.Any() ? " (" + string.Join(", ", cond.Select(kv => kv.Key + " " + Value(kv.Value))) + ")" : ""; + rows.Add(new Row { Entry = o.GetString("id"), Kind = o.GetString("kind"), Quantity = q.Key, Values = string.Join("; ", vals) + conditions, + Status = qo.GetString("status", qo.Has("ref") ? "see model" : "?"), Uncertainty = unc, Source = qo.GetString("source", qo.Has("ref") ? "the Raman model '" + qo.GetString("ref") + "'" : ""), Check = qo.GetString("verification", "") }); + } + + var sb = new StringBuilder(); + sb.AppendLine("# Material catalogue"); + sb.AppendLine(); + sb.AppendLine("*Generated by `ultrafast reference materials` from `src/UltrafastEngine.Core/Data/materials/`; do not edit. Change a number in its catalogue file, then run `/regen`.*"); + sb.AppendLine(); + sb.AppendLine("Every number the engine takes from its material library is listed here with its unit (in the key name), its **status**, its uncertainty and its source."); + sb.AppendLine("How the files work, how to override a value and how runs report the values they used: [Materials](materials.md#the-material-catalogue)."); + sb.AppendLine(); + sb.AppendLine("| status | meaning |"); + sb.AppendLine("|---|---|"); + sb.AppendLine("| `verified` | the number was read from the cited source and compared; the comparison is recorded (`check`) |"); + sb.AppendLine("| `approximate` | a typical, rounded or fitted value, good to the stated uncertainty; not traced to one table |"); + sb.AppendLine("| `unverified` | not checked against any source; quoted from memory or from an earlier version of the code |"); + sb.AppendLine(); + sb.AppendLine("A run that uses an `approximate` or `unverified` value says so in its notes and lists it under `unverifiedInputs` in `summary.json`."); + sb.AppendLine(); + int n = rows.Count, v = rows.Count(r => r.Status == "verified"), a = rows.Count(r => r.Status == "approximate"), u = rows.Count(r => r.Status == "unverified"); + sb.AppendLine("**" + n + " quantities in " + entries.Count + " entries: " + v + " verified, " + a + " approximate, " + u + " unverified.**"); + sb.AppendLine(); + + foreach (var (kind, title) in new[] { ("solid", "Solids and liquids"), ("gas", "Gases"), ("raman", "Raman models"), ("gain", "Gain media"), ("crystal", "chi(2) crystals"), ("default", "Generic defaults (used when a run does not give the value)") }) + { + sb.AppendLine("## " + title); + sb.AppendLine(); + foreach (var g in rows.Where(r => r.Kind == kind).GroupBy(r => r.Entry)) + { + var o = entries.First(e => e.O.GetString("id") == g.Key).O; + sb.AppendLine("### " + g.Key); + sb.AppendLine(); + sb.Append(o.GetString("description", "")); + var al = o.GetArray("aliases")?.Select(x => x.AsString()).Where(x => x.Length > 0).ToList(); + if (al != null && al.Count > 0) sb.Append(". Aliases: " + string.Join(", ", al.Select(x => "`" + x + "`"))); + sb.AppendLine("."); + if (o.Has("notes")) { sb.AppendLine(); sb.AppendLine(o.GetString("notes") + (o.GetString("notes").EndsWith(".") ? "" : ".")); } + sb.AppendLine(); + sb.AppendLine("| quantity | values | status | uncertainty | source |"); + sb.AppendLine("|---|---|---|---|---|"); + foreach (var r in g) + sb.AppendLine("| `" + r.Quantity + "` | " + Cell(r.Values) + " | " + r.Status + " | " + Cell(r.Uncertainty) + " | " + Cell(r.Source + (r.Check.Length > 0 ? " Check: " + r.Check + "." : "")) + " |"); + sb.AppendLine(); + } + } + + sb.AppendLine("## Not verified yet"); + sb.AppendLine(); + sb.AppendLine("The to-do list for anyone with the papers: every quantity whose status is not `verified`."); + sb.AppendLine(); + sb.AppendLine("| entry | quantity | status | what to check |"); + sb.AppendLine("|---|---|---|---|"); + foreach (var r in rows.Where(r => r.Status == "approximate" || r.Status == "unverified")) + sb.AppendLine("| " + r.Entry + " | `" + r.Quantity + "` | " + r.Status + " | " + Cell(r.Source) + " |"); + return sb.ToString(); + } + } +} diff --git a/src/UltrafastEngine.Core/Materials/Gases.cs b/src/UltrafastEngine.Core/Materials/Gases.cs index 1dbd853..cd6714f 100644 --- a/src/UltrafastEngine.Core/Materials/Gases.cs +++ b/src/UltrafastEngine.Core/Materials/Gases.cs @@ -9,10 +9,9 @@ namespace UltrafastEngine.Materials { /// - /// Data of the built-in gases (docs/reference/gases.md). Values are literature values good to ~10-30 % - /// (electronic n2, polarisability anisotropy, dephasing times); every one can be overridden in a material - /// definition. References: rotational constants (NIST / Herzberg); anisotropies Bridge & Buckingham, Proc. R. Soc. - /// A 295, 334 (1966); electronic n2 Wahlstrand, Cheng & Milchberg, PRA 85, 043820 (2012); ionisation potentials NIST. + /// Data of the built-in gases (docs/reference/gases.md), read from the catalogue (Data/materials/gases, one file per gas, with the source and + /// status of each number: docs/reference/materials-catalogue.md). Values are literature values good to ~10-30 % + /// (electronic n2, polarisability anisotropy, dephasing times); every one can be overridden in a material definition. /// public sealed class GasData { @@ -28,21 +27,40 @@ public sealed class GasData public double RotB, RotD, AnisotropyA3, GEven, GOdd, T2At1Bar; public bool Molecular => RotB > 0; - public static readonly Dictionary Table = new Dictionary(StringComparer.OrdinalIgnoreCase) + /// The catalogue entry of the gas (its quantities carry the source and status of each number). + public CatalogueEntry Entry; + + /// The gases with ionisation data, from the catalogue (Data/materials/gases). Air is a mixture and is not in the table. + public static Dictionary Table => Catalogue.Cached("gasdata.table", () => + { + var t = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var e in Catalogue.Entries("gas")) + { + var ion = e.TryQ("ionization"); + if (ion == null) continue; + var g = new GasData { Species = e.Id, Entry = e, IonizationEv = ion.Number("ionizationEv"), L = (int)Math.Round(ion.Number("orbitalL")), ZEff = ion.Number("zEff"), N2Electronic = e.Q("n2Electronic").Number("valueM2PerWPerBar273K") }; + var rot = e.TryQ("rotation"); + if (rot != null) + { + g.RotB = rot.Number("rotationalConstantBPerCm"); g.RotD = rot.Number("centrifugalDistortionDPerCm"); g.AnisotropyA3 = rot.Number("polarizabilityAnisotropyA3"); + g.GEven = rot.Number("nuclearSpinWeightEven"); g.GOdd = rot.Number("nuclearSpinWeightOdd"); + g.T2At1Bar = e.Q("dephasing").Number("t2At1BarS"); + } + t[e.Id] = g; + } + return t; + }); + + /// Mole fractions of dry air used for its nonlinear and ionisation responses (catalogue entry Air, quantity "composition"). + public static (string Species, double Fraction)[] AirComposition => Catalogue.Cached("gasdata.air", () => { - { "He", new GasData { Species = "He", IonizationEv = 24.587, L = 0, N2Electronic = 3.5e-25 } }, - { "Ne", new GasData { Species = "Ne", IonizationEv = 21.565, L = 1, N2Electronic = 7.5e-25 } }, - { "Ar", new GasData { Species = "Ar", IonizationEv = 15.760, L = 1, N2Electronic = 1.0e-23 } }, - { "Kr", new GasData { Species = "Kr", IonizationEv = 13.999, L = 1, N2Electronic = 2.8e-23 } }, - { "Xe", new GasData { Species = "Xe", IonizationEv = 12.130, L = 1, N2Electronic = 8.0e-23 } }, - { "N2", new GasData { Species = "N2", IonizationEv = 15.581, L = 0, ZEff = 0.9, N2Electronic = 8.0e-24, RotB = 1.98957, RotD = 5.76e-6, AnisotropyA3 = 0.70, GEven = 6, GOdd = 3, T2At1Bar = 100e-12 } }, - { "O2", new GasData { Species = "O2", IonizationEv = 12.070, L = 1, ZEff = 0.53, N2Electronic = 1.03e-23, RotB = 1.43768, RotD = 4.85e-6, AnisotropyA3 = 1.14, GEven = 0, GOdd = 1, T2At1Bar = 80e-12 } }, - { "CO2", new GasData { Species = "CO2", IonizationEv = 13.777, L = 1, ZEff = 1.0, N2Electronic = 1.4e-23, RotB = 0.39021, RotD = 1.33e-7, AnisotropyA3 = 2.10, GEven = 1, GOdd = 0, T2At1Bar = 50e-12 } }, - { "H2", new GasData { Species = "H2", IonizationEv = 15.426, L = 0, ZEff = 1.0, N2Electronic = 6.9e-24, RotB = 59.322, RotD = 0.0471, AnisotropyA3 = 0.30, GEven = 1, GOdd = 3, T2At1Bar = 300e-12 } }, - }; - - /// Mole fractions of dry air used for its nonlinear and ionisation responses. - public static readonly (string Species, double Fraction)[] AirComposition = { ("N2", 0.7808), ("O2", 0.2095), ("Ar", 0.0093) }; + var c = Catalogue.Get("Air", "gas").Q("composition").Json.GetObject("fractions"); + return c.Select(kv => (kv.Key, kv.Value.AsDouble())).ToArray(); + }); + + /// Electronic n2 per bar at 273 K of every gas of the catalogue, including Air (m^2/W). + public static Dictionary N2PerBar => Catalogue.Cached("gasdata.n2", () => + Catalogue.Entries("gas").Where(e => e.Has("n2Electronic")).ToDictionary(e => e.Id, e => e.Q("n2Electronic").Number("valueM2PerWPerBar273K"), StringComparer.OrdinalIgnoreCase)); public static GasData Get(string species) => species != null && Table.TryGetValue(species, out var g) ? g : null; } @@ -168,12 +186,16 @@ public double[] Convolve(double[] x, double dt) /// public sealed class GasFormulaIndex : Models.IRefractiveIndex { - private static readonly Dictionary Forms = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static Dictionary Forms => Catalogue.Cached("gasformula.forms", () => { - { "O2", (1.181494e-4, new[] { 9.708931e-3 }, new[] { 75.4 }, 293.15) }, - { "CO2", (0, new[] { 6.99100e-2, 1.44720e-3, 6.42941e-5, 5.21306e-5, 1.46847e-6 }, new[] { 166.175, 79.609, 56.3064, 46.0196, 0.0584738 }, 273.15) }, - { "H2", (0, new[] { 0.0148956, 0.0049037 }, new[] { 180.7, 92.0 }, 273.15) }, - }; + var d = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var e in Catalogue.Entries("gas")) + { + var q = e.TryQ("index"); + if (q != null && q.Text("model") == "dispersionFormula") d[e.Id] = (q.Number("c0"), q.Array("b"), q.Array("cPerUm2"), q.Number("referenceTemperatureK")); + } + return d; + }); public static bool Knows(string species) => Forms.ContainsKey(species); public static IEnumerable Species => Forms.Keys; diff --git a/src/UltrafastEngine.Core/Materials/IndexModels.cs b/src/UltrafastEngine.Core/Materials/IndexModels.cs index fa5870f..fcfb722 100644 --- a/src/UltrafastEngine.Core/Materials/IndexModels.cs +++ b/src/UltrafastEngine.Core/Materials/IndexModels.cs @@ -180,58 +180,87 @@ public void Evaluate(double[] omega, double[] nOut) /// public sealed class CiddorAirIndex : IRefractiveIndex { + /// The constants of Ciddor's formulas, from the catalogue entry Air (quantities standardAirDispersion, co2Dependence, waterVaporDispersion, thermodynamics, saturationVapourPressure, + /// enhancementFactor, compressibility, referenceStates), read once per loaded catalogue. + private sealed class Constants + { + public double K0, K1, K2, K3, Co2Slope, Co2Ref, MaMolar, MaSlope, MaRef, Cf, R, Mw, Pstd, Tstd, Pw, Tw; + public double[] W, Svp, Enh, Zc; + public double T0C, P0Pa, Rh0, Co20Ppm; + } + + private static Constants K => Catalogue.Cached("ciddor.constants", () => + { + var air = Catalogue.Get("Air", "gas"); + var sd = air.Q("standardAirDispersion"); var co = air.Q("co2Dependence"); var wv = air.Q("waterVaporDispersion"); var th = air.Q("thermodynamics"); var rs = air.Q("referenceStates"); var ix = air.Q("index"); + return new Constants + { + K0 = sd.Number("k0PerUm2"), K1 = sd.Number("k1PerUm2"), K2 = sd.Number("k2PerUm2"), K3 = sd.Number("k3PerUm2"), + Co2Slope = co.Number("co2SlopeFractionPerPpm"), Co2Ref = co.Number("co2ReferencePpm"), MaMolar = co.Number("airMolarMassGPerMol"), MaSlope = co.Number("airMolarMassSlopeGPerMolPerPpm"), MaRef = co.Number("airMolarMassReferencePpm"), + W = wv.Array("polynomialCoefficients"), Cf = wv.Number("correctionFactor"), R = th.Number("gasConstantJPerMolK"), Mw = th.Number("waterMolarMassKgPerMol"), + Svp = air.Q("saturationVapourPressure").Array("coefficients"), Enh = air.Q("enhancementFactor").Array("coefficients"), Zc = air.Q("compressibility").Array("coefficients"), + Pstd = rs.Number("standardAirPressurePa"), Tstd = rs.Number("standardAirTemperatureK"), Pw = rs.Number("waterVaporPressurePa"), Tw = rs.Number("waterVaporTemperatureK"), + T0C = ix.Number("temperatureC"), P0Pa = ix.Number("pressurePa"), Rh0 = ix.Number("relativeHumidityPercent"), Co20Ppm = ix.Number("co2Ppm") + }; + }); + public double TemperatureC { get; } public double PressurePa { get; } public double RelativeHumidity { get; } public double Co2Ppm { get; } + private static double Svp(Constants k, double T) => Math.Exp(k.Svp[0] * T * T + k.Svp[1] * T + k.Svp[2] + k.Svp[3] / T); + private static double Enhancement(Constants k, double p, double t) => k.Enh[0] + k.Enh[1] * p + k.Enh[2] * t * t; + /// Mole fraction of water vapour: enhancement factor x RH x saturation vapour pressure / pressure (CIPM-2007 / Ciddor). public double WaterVaporMoleFraction { get { - double T = TemperatureC + 273.15, p = PressurePa, t = TemperatureC; - double svp = Math.Exp(1.2378847e-5 * T * T - 1.9121316e-2 * T + 33.93711047 - 6.3431645e3 / T); - return (1.00062 + 3.14e-8 * p + 5.6e-7 * t * t) * RelativeHumidity * svp / p; + var k = K; double T = TemperatureC + 273.15, p = PressurePa, t = TemperatureC; + return Enhancement(k, p, t) * RelativeHumidity * Svp(k, T) / p; } } - public CiddorAirIndex(double temperatureC = 20.0, double pressurePa = 101325.0, double relativeHumidity = 0.0, double co2Ppm = 450.0) + /// Air at the given state; parameters not given (NaN) take the standard environment of the catalogue entry Air (20 C, 101325 Pa, dry, 450 ppm CO2). + public CiddorAirIndex(double temperatureC = double.NaN, double pressurePa = double.NaN, double relativeHumidity = double.NaN, double co2Ppm = double.NaN) { - TemperatureC = temperatureC; PressurePa = pressurePa; RelativeHumidity = relativeHumidity; Co2Ppm = co2Ppm; + var k = K; + TemperatureC = double.IsNaN(temperatureC) ? k.T0C : temperatureC; PressurePa = double.IsNaN(pressurePa) ? k.P0Pa : pressurePa; + RelativeHumidity = double.IsNaN(relativeHumidity) ? k.Rh0 : relativeHumidity; Co2Ppm = double.IsNaN(co2Ppm) ? k.Co20Ppm : co2Ppm; } public string Description => string.Format(System.Globalization.CultureInfo.InvariantCulture, "ciddor air ({0:G4} C, {1:G6} Pa, RH {2:G3}, CO2 {3:G4} ppm)", TemperatureC, PressurePa, RelativeHumidity, Co2Ppm); - private static double Z(double p, double T, double xw) + private static double Z(Constants k, double p, double T, double xw) { - const double a0 = 1.58123e-6, a1 = -2.9331e-8, a2 = 1.1043e-10, b0 = 5.707e-6, b1 = -2.051e-8, - c0 = 1.9898e-4, c1 = -2.376e-6, d = 1.83e-11, e = -0.765e-8; + double a0 = k.Zc[0], a1 = k.Zc[1], a2 = k.Zc[2], b0 = k.Zc[3], b1 = k.Zc[4], c0 = k.Zc[5], c1 = k.Zc[6], d = k.Zc[7], e = k.Zc[8]; double t = T - 273.15; return 1 - (p / T) * (a0 + a1 * t + a2 * t * t + (b0 + b1 * t) * xw + (c0 + c1 * t) * xw * xw) + (p / T) * (p / T) * (d + e * xw * xw); } public void Evaluate(double[] omega, double[] nOut) { - const double k0 = 238.0185, k1 = 5792105, k2 = 57.362, k3 = 167917; - const double w0 = 295.235, w1 = 2.6422, w2 = -0.032380, w3 = 0.004028, cf = 1.022; - const double R = 8.314510, Mw = 0.018015; + var kc = K; + double k0 = kc.K0, k1 = kc.K1, k2 = kc.K2, k3 = kc.K3; + double w0 = kc.W[0], w1 = kc.W[1], w2 = kc.W[2], w3 = kc.W[3], cf = kc.Cf; + double R = kc.R, Mw = kc.Mw; double T = TemperatureC + 273.15, p = PressurePa, t = TemperatureC; - double Ma = 1e-3 * (28.9635 + 12.011e-6 * (Co2Ppm - 400)); - double svp = Math.Exp(1.2378847e-5 * T * T - 1.9121316e-2 * T + 33.93711047 - 6.3431645e3 / T); - double f = 1.00062 + 3.14e-8 * p + 5.6e-7 * t * t; + double Ma = 1e-3 * (kc.MaMolar + kc.MaSlope * (Co2Ppm - kc.MaRef)); + double svp = Svp(kc, T); + double f = Enhancement(kc, p, t); double xw = f * RelativeHumidity * svp / p; - double Za = Z(101325, 288.15, 0), Zw = Z(1333, 293.15, 1), Zm = Z(p, T, xw); - double rhoAxs = 101325 * Ma / (Za * R * 288.15); - double rhoWs = 1333 * Mw / (Zw * R * 293.15); + double Za = Z(kc, kc.Pstd, kc.Tstd, 0), Zw = Z(kc, kc.Pw, kc.Tw, 1), Zm = Z(kc, p, T, xw); + double rhoAxs = kc.Pstd * Ma / (Za * R * kc.Tstd); + double rhoWs = kc.Pw * Mw / (Zw * R * kc.Tw); double rhoA = p * Ma * (1 - xw) / (Zm * R * T); double rhoW = p * Mw * xw / (Zm * R * T); for (int i = 0; i < omega.Length; i++) { double s2 = 1.0 / Wl.OmegaToLambdaUm(omega[i]); s2 *= s2; double nas = 1e-8 * (k1 / (k0 - s2) + k3 / (k2 - s2)); - double naxs = nas * (1 + 0.534e-6 * (Co2Ppm - 450)); + double naxs = nas * (1 + kc.Co2Slope * (Co2Ppm - kc.Co2Ref)); double nws = 1e-8 * cf * (w0 + w1 * s2 + w2 * s2 * s2 + w3 * s2 * s2 * s2); nOut[i] = 1 + (rhoA / rhoAxs) * naxs + (rhoW / rhoWs) * nws; } @@ -245,16 +274,19 @@ public void Evaluate(double[] omega, double[] nOut) /// public sealed class GasIndex : IRefractiveIndex { - public static readonly Dictionary Coefficients = new Dictionary(StringComparer.OrdinalIgnoreCase) + /// The four Borzsonyi coefficients of each gas (n^2 - 1 = f (B1 l^2 / (l^2 - C1) + B2 l^2 / (l^2 - C2)), l in um), from the catalogue. + public static Dictionary Coefficients => Catalogue.Cached("gasindex.coefficients", () => { - { "He", new[] { 4977.77e-8, 28.54e-6, 1856.94e-8, 7.760e-3 } }, - { "Ne", new[] { 9154.48e-8, 656.97e-6, 4018.63e-8, 5.728e-3 } }, - { "Ar", new[] { 20332.29e-8, 206.12e-6, 34458.31e-8, 8.066e-3 } }, - { "Kr", new[] { 26102.88e-8, 2.01e-6, 56946.82e-8, 10.043e-3 } }, - { "Xe", new[] { 103701.61e-8, 12.75e-3, 31228.61e-8, 0.561e-3 } }, - { "N2", new[] { 39209.95e-8, 1146.24e-6, 18806.48e-8, 13.476e-3 } }, - { "Air", new[] { 14926.44e-8, 19.36e-6, 41807.57e-8, 7.434e-3 } }, - }; + var d = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var e in Catalogue.Entries("gas")) + { + var q = e.TryQ("index"); + if (q == null) continue; + if (q.Text("model") == "borzsonyi") d[e.Id] = q.Array("coefficients"); + else if (q.Has("borzsonyiCoefficients")) d[e.Id] = q.Array("borzsonyiCoefficients"); + } + return d; + }); public string Species { get; } public double PressureBar { get; } diff --git a/src/UltrafastEngine.Core/Materials/Material.cs b/src/UltrafastEngine.Core/Materials/Material.cs index 0778ba7..ce5fb9a 100644 --- a/src/UltrafastEngine.Core/Materials/Material.cs +++ b/src/UltrafastEngine.Core/Materials/Material.cs @@ -68,6 +68,9 @@ public sealed class Material /// Photo-ionisation data of a solid (band gap etc.), used by the plasma model; null for gases (taken from GasData). public SolidIonizationData Ionization { get; set; } public string Notes { get; set; } + /// Catalogue quantities this material takes its numbers from, by property ("index", "n2", "raman", "ramanFraction", "kerrAnisotropy", + /// "rotation:N2", "dephasing:N2", ...). A property the user overrides is removed, so a run reports only the catalogue values it really used. + public Dictionary Used { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// Provenance of each property (property name -> description + source). public Dictionary Provenance { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -99,6 +102,7 @@ public Material Clone() Rotational = Rotational, GasSpecies = GasSpecies, GasPressureBar = GasPressureBar, GasTemperatureK = GasTemperatureK, Ionization = Ionization }; foreach (var kv in Provenance) copy.Provenance[kv.Key] = kv.Value; + foreach (var kv in Used) copy.Used[kv.Key] = kv.Value; return copy; } @@ -193,25 +197,32 @@ public sealed class SolidIonizationData { public double BandGapEv; /// Reduced effective mass / electron mass. - public double EffectiveMass = 0.635; + public double EffectiveMass; /// Density of valence electrons that can be ionised (1/m^3). - public double NeutralDensityM3 = 2.1e28; + public double NeutralDensityM3; /// Electron collision time (s) and recombination / trapping time (s). - public double CollisionTimeS = 1e-15; - public double RecombinationTimeS = 150e-15; + public double CollisionTimeS; + public double RecombinationTimeS; + + /// The generic dielectric inputs (catalogue entry "defaults:solidIonization"), used where a run gives some but not all of them. + public static CatalogueQuantity DefaultsQuantity => Catalogue.Get("defaults:solidIonization", "default").Q("ionization"); + + public SolidIonizationData() + { + var q = DefaultsQuantity; + EffectiveMass = q.Number("effectiveMassRatio"); NeutralDensityM3 = q.Number("neutralDensityM3"); + CollisionTimeS = q.Number("collisionTimeS"); RecombinationTimeS = q.Number("recombinationTimeS"); + } + + /// The catalogue quantity "ionization" of a solid (null when the material has none). + public static CatalogueQuantity CatalogueQuantityOf(string material) => Catalogue.Find(material, "solid")?.TryQ("ionization"); public static SolidIonizationData For(string material) { - switch ((MaterialLibrary.Canonical(material) ?? "").ToLowerInvariant()) - { - case "fusedsilica": case "quartz": case "quartz-e": return new SolidIonizationData { BandGapEv = 9.0, EffectiveMass = 0.635, NeutralDensityM3 = 2.1e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - case "sapphire": case "sapphire-e": return new SolidIonizationData { BandGapEv = 8.8, EffectiveMass = 0.5, NeutralDensityM3 = 2.3e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - case "caf2": return new SolidIonizationData { BandGapEv = 10.0, EffectiveMass = 0.5, NeutralDensityM3 = 2.4e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - case "yag": return new SolidIonizationData { BandGapEv = 6.5, EffectiveMass = 0.5, NeutralDensityM3 = 2.0e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - case "mgf2": case "mgf2-e": return new SolidIonizationData { BandGapEv = 10.8, EffectiveMass = 0.5, NeutralDensityM3 = 2.0e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - case "n-bk7": return new SolidIonizationData { BandGapEv = 4.2, EffectiveMass = 0.5, NeutralDensityM3 = 2.0e28, CollisionTimeS = 1e-15, RecombinationTimeS = 150e-15 }; - default: return null; - } + var q = CatalogueQuantityOf(material); + if (q == null) return null; + return new SolidIonizationData { BandGapEv = q.Number("bandGapEv"), EffectiveMass = q.Number("effectiveMassRatio"), NeutralDensityM3 = q.Number("neutralDensityM3"), + CollisionTimeS = q.Number("collisionTimeS"), RecombinationTimeS = q.Number("recombinationTimeS") }; } } } diff --git a/src/UltrafastEngine.Core/Materials/MaterialLibrary.cs b/src/UltrafastEngine.Core/Materials/MaterialLibrary.cs index 62a69e4..f15e400 100644 --- a/src/UltrafastEngine.Core/Materials/MaterialLibrary.cs +++ b/src/UltrafastEngine.Core/Materials/MaterialLibrary.cs @@ -16,173 +16,101 @@ namespace UltrafastEngine.Materials /// public static class MaterialLibrary { - private static readonly Dictionary Aliases = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "F_SILICA", "FusedSilica" }, { "SILICA", "FusedSilica" }, { "FUSED_SILICA", "FusedSilica" }, { "FS", "FusedSilica" }, { "SIO2", "FusedSilica" }, { "UVFS", "FusedSilica" }, - { "BK7", "N-BK7" }, { "NBK7", "N-BK7" }, - { "CAF2", "CaF2" }, { "SAPPHIRE", "Sapphire" }, { "AL2O3", "Sapphire" }, - { "YAG", "YAG" }, { "SF11", "N-SF11" }, { "NSF11", "N-SF11" }, { "MGF2", "MgF2" }, - { "QUARTZ", "Quartz" }, { "CRYSTALQUARTZ", "Quartz" }, { "SIO2_CRYSTAL", "Quartz" }, { "QUARTZ-O", "Quartz" }, { "MGF2-O", "MgF2" }, { "SAPPHIRE-O", "Sapphire" }, - { "CALCITE-O", "Calcite" }, { "CACO3", "Calcite" }, { "YVO4-O", "YVO4" }, { "ABBO", "aBBO" }, { "ALPHA-BBO", "aBBO" }, { "ABBO-O", "aBBO" }, { "A-BBO", "aBBO" }, - { "CARBONDISULFIDE", "CS2" }, { "CARBON_DISULFIDE", "CS2" }, { "BAF2", "BaF2" }, - { "AIR", "Air" }, { "VACUUM", "Vacuum" }, { "", "Air" }, { "OXYGEN", "O2" }, { "HYDROGEN", "H2" }, { "CARBONDIOXIDE", "CO2" }, - { "TA2O5", "Ta2O5" }, { "TIO2", "TiO2" }, { "HFO2", "HfO2" }, { "NB2O5", "Nb2O5" }, { "AL2O3_FILM", "Al2O3_film" }, { "SIO2_FILM", "SiO2_film" }, - { "HELIUM", "He" }, { "NEON", "Ne" }, { "ARGON", "Ar" }, { "KRYPTON", "Kr" }, { "XENON", "Xe" }, { "NITROGEN", "N2" } - }; + /// Names of the built-in materials and gases, in catalogue order (the Raman models are not materials). + public static IReadOnlyList BuiltInNames => Catalogue.Entries().Where(IsMaterialEntry).Select(e => e.Id).ToArray(); - public static IReadOnlyList BuiltInNames => new[] { "Vacuum", "Air", "FusedSilica", "N-BK7", "CaF2", "Sapphire", "YAG", "N-SF11", "MgF2", "He", "Ne", "Ar", "Kr", "Xe", "N2", "O2", "CO2", "H2", - "Quartz", "Quartz-e", "MgF2-e", "Sapphire-e", "Calcite", "Calcite-e", "YVO4", "YVO4-e", "aBBO", "aBBO-e", "TGG", "CS2", "BaF2", - "Ta2O5", "TiO2", "HfO2", "Nb2O5", "Al2O3_film", "SiO2_film" }; + /// Solids, liquids and gases are materials; Raman models, gain media and crystals are other catalogue kinds. + private static bool IsMaterialEntry(CatalogueEntry e) => e.Kind == "solid" || e.Kind == "gas"; - public static string Canonical(string name) => name != null && Aliases.TryGetValue(name.Trim(), out var c) ? c : name?.Trim(); + public static string Canonical(string name) => name == null ? null : Catalogue.Find(name) is CatalogueEntry e && IsMaterialEntry(e) ? e.Id : name.Trim(); - public static bool IsBuiltIn(string name) => BuiltInNames.Contains(Canonical(name), StringComparer.OrdinalIgnoreCase); + public static bool IsBuiltIn(string name) => name != null && Catalogue.Find(name) is CatalogueEntry e && IsMaterialEntry(e); - /// Approximate nonlinear indices of gases at 1 bar, 273 K, ~800 nm (m^2/W); scaled with density. - /// Values follow the commonly used compilation (Lehmeier 1985 / Borzsonyi 2010 / Wahlstrand 2012) and - /// vary by ~20 % between sources. - public static readonly Dictionary GasN2PerBar = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - // electronic (instantaneous) part; molecular gases add their rotational Raman response (GasData, docs/reference/gases.md) - { "He", 3.5e-25 }, { "Ne", 7.5e-25 }, { "Ar", 1.0e-23 }, { "Kr", 2.8e-23 }, { "Xe", 8.0e-23 }, { "N2", 8.0e-24 }, { "O2", 1.03e-23 }, { "CO2", 1.4e-23 }, { "H2", 6.9e-24 }, { "Air", 8.5e-24 } - }; + /// Electronic nonlinear indices of gases at 1 bar, 273 K, ~800 nm (m^2/W); scaled with density. From the catalogue (Data/materials/gases), + /// where each value carries its source and status. + public static IReadOnlyDictionary GasN2PerBar => GasData.N2PerBar; /// A built-in material by name (or alias, e.g. Zemax's "F_SILICA"). Gases at 1 bar / 293 K. public static Material BuiltIn(string name, double gasPressureBar = 1.0, double gasTemperatureK = 293.15) { - string c = Canonical(name); - var air = new CiddorAirIndex(); + var e = Catalogue.Find(name); + if (e == null || !IsMaterialEntry(e)) + throw new ModelException("Unknown material \"" + name + "\". Built-in materials: " + string.Join(", ", BuiltInNames) + ". Define it in the system file's \"materials\" section (e.g. as an index table), or add a catalogue file (docs/reference/materials-catalogue.md)."); Material m; - switch (c?.ToLowerInvariant()) - { - case "vacuum": - m = new Material { Name = "Vacuum", Index = new ConstantIndex(1.0) }; - break; - case "air": - m = new Material { Name = "Air", Index = air, IsGas = true, N2 = GasN2PerBar["Air"] * 1.01325 * 273.0 / 293.15, Notes = "Ciddor 1996 at 20 C, 101325 Pa, 0 % RH, 450 ppm CO2; electronic n2 + rotational Raman of N2 and O2", - Rotational = RotationalRamanResponse.For(GasData.AirComposition, 1.01325, 293.15), GasSpecies = "Air", GasPressureBar = 1.01325, GasTemperatureK = 293.15 }; - break; - case "fusedsilica": - m = Glass("FusedSilica", new SellmeierIndex(new[] { 0.6961663, 0.4079426, 0.8974794 }, new[] { 0.0684043 * 0.0684043, 0.1162414 * 0.1162414, 9.896161 * 9.896161 }, 1.0, "Malitson 1965"), - air, 2.6e-20, 0.18, new AnisotropicSplitResponse(new DampedOscillatorResponse(12.2e-15, 32e-15), 0.25)); - break; - case "n-bk7": - m = Glass("N-BK7", new SellmeierIndex(new[] { 1.03961212, 0.231792344, 1.01046945 }, new[] { 0.00600069867, 0.0200179144, 103.560653 }, 1.0, "Schott"), air, 3.4e-20, 0.0, null); - break; - case "caf2": - m = Glass("CaF2", new SellmeierIndex(new[] { 0.5675888, 0.4710914, 3.8484723 }, new[] { 0.050263605 * 0.050263605, 0.1003909 * 0.1003909, 34.649040 * 34.649040 }, 1.0, "Malitson 1963"), air, 1.9e-20, 0.0, null); - break; - case "sapphire": - m = Glass("Sapphire", new SellmeierIndex(new[] { 1.4313493, 0.65054713, 5.3414021 }, new[] { 0.0726631 * 0.0726631, 0.1193242 * 0.1193242, 18.028251 * 18.028251 }, 1.0, "Malitson & Dodge 1972, ordinary"), air, 3.0e-20, 0.0, null); - break; - case "yag": - m = Glass("YAG", new SellmeierIndex(new[] { 2.282, 3.27644 }, new[] { 0.01185, 282.734 }, 1.0, "Zelmon 1998"), air, 6.2e-20, 0.0, null); - break; - case "n-sf11": - m = Glass("N-SF11", new SellmeierIndex(new[] { 1.73759695, 0.313747346, 1.89878101 }, new[] { 0.013188707, 0.0623068142, 155.23629 }, 1.0, "Schott"), air, 1.0e-19, 0.0, null); - break; - case "mgf2": - m = Glass("MgF2", new SellmeierIndex(new[] { 0.48755108, 0.39875031, 2.3120353 }, new[] { 0.04338408 * 0.04338408, 0.09461442 * 0.09461442, 23.793604 * 23.793604 }, 1.0, "Dodge 1984, ordinary"), air, 0.9e-20, 0.0, null); - break; - // birefringent crystals (ordinary = the plain name, extraordinary = "-e"); BirefringentMaterials pairs them - case "quartz": - m = Glass("Quartz", new SellmeierIndex(new[] { 1.07044083, 1.10202242 }, new[] { 1.00585997e-2, 100.0 }, 1.28604141, "Ghosh 1999, crystalline quartz, ordinary"), air, 3.0e-20, 0.0, null); - break; - case "quartz-e": - m = Glass("Quartz-e", new SellmeierIndex(new[] { 1.09509924, 1.15662475 }, new[] { 1.02101864e-2, 100.0 }, 1.28851804, "Ghosh 1999, crystalline quartz, extraordinary"), air, 3.0e-20, 0.0, null); - break; - case "mgf2-e": - m = Glass("MgF2-e", new SellmeierIndex(new[] { 0.41344023, 0.50497499, 2.4904862 }, new[] { 0.03684262 * 0.03684262, 0.09076162 * 0.09076162, 23.771995 * 23.771995 }, 1.0, "Dodge 1984, extraordinary"), air, 0.9e-20, 0.0, null); - break; - case "sapphire-e": - m = Glass("Sapphire-e", new SellmeierIndex(new[] { 1.5039759, 0.55069141, 6.5927379 }, new[] { 0.0740288 * 0.0740288, 0.1216529 * 0.1216529, 20.072248 * 20.072248 }, 1.0, "Malitson & Dodge 1972, extraordinary"), air, 3.0e-20, 0.0, null); - break; - case "calcite": - m = Glass("Calcite", new SellmeierIndex(new[] { 0.96464345, 1.82831454 }, new[] { 1.94325203e-2, 120.0 }, 1.73358749, "Ghosh 1999, ordinary"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "calcite-e": - m = Glass("Calcite-e", new SellmeierIndex(new[] { 0.82427830, 0.14429128 }, new[] { 1.06689543e-2, 120.0 }, 1.35859695, "Ghosh 1999, extraordinary"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "yvo4": - m = Glass("YVO4", new SellmeierIrIndex(3.77834 - 0.069736 / 0.04724, new[] { 0.069736 / 0.04724 }, new[] { 0.04724 }, 0.0108133, "YVO4 ordinary (common vendor Sellmeier)"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "yvo4-e": - m = Glass("YVO4-e", new SellmeierIrIndex(4.59905 - 0.110534 / 0.04813, new[] { 0.110534 / 0.04813 }, new[] { 0.04813 }, 0.0122676, "YVO4 extraordinary (common vendor Sellmeier)"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "abbo": - m = Glass("aBBO", new SellmeierIrIndex(2.67579 - 0.02099 / 0.00470, new[] { 0.02099 / 0.00470 }, new[] { 0.00470 }, 0.00528, "alpha-BBO ordinary (vendor Sellmeier)"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "abbo-e": - m = Glass("aBBO-e", new SellmeierIrIndex(2.31197 - 0.01184 / 0.01607, new[] { 0.01184 / 0.01607 }, new[] { 0.01607 }, 0.00400, "alpha-BBO extraordinary (vendor Sellmeier)"), air, 0.0, 0.0, null); - m.Notes = "n2 not set (give one if it matters)"; - break; - case "tgg": - // two-point single-pole fit (n = 1.954 at 1064 nm, 1.974 at 633 nm): approximate; override for precision work - m = Glass("TGG", new SellmeierIndex(new[] { 2.7769 }, new[] { 0.01656 }, 1.0, "TGG, approximate single-pole fit"), air, 0.0, 0.0, null); - m.Notes = "approximate dispersion (two-point fit); n2 not set (give one: the B-integral of an isolator matters)"; - break; - case "baf2": - // cubic fluoride (m3m). Index: Li, J. Phys. Chem. Ref. Data 9, 161 (1980), 293 K, 0.15-15 um (refractiveindex.info, CC0). - m = Glass("BaF2", new SellmeierIndex(new[] { 0.81070, 0.19652, 4.52469 }, new[] { 0.10065 * 0.10065, 29.87 * 29.87, 53.82 * 53.82 }, 1.33973, "Li 1980"), air, BaF2N2PerW, 0.0, null); - m.KerrAnisotropy = BaF2Sigma; - m.Notes = "cubic (m3m); n2 = 2.0e-20 m^2/W for light polarised along [100] and anisotropy sigma = (chi1111 - 3 chi1122) / chi1111 = -1.2 are UNVERIFIED literature-typical " - + "values (n2 spread 1.8-2.9e-20; sigma from Minkovski et al., Opt. Lett. 29, 2237 (2004) / Jullien et al., J. Opt. Soc. Am. B 22, 2635 (2005), quoted from memory, not retrieved): override n2 / kerrAnisotropy. " - + "Orientation: give kerrCutAxis / kerrAzimuthDeg on the element (default [001] cut, x along [100])"; - break; - case "cs2": - // liquid carbon disulfide, 20 C. Index: Kedenburg et al. 2012 (refractiveindex.info, CC0), one-pole fit valid 0.5-1.6 um. - // Nonlinearity: electronic (instantaneous, isotropic Maker-Terhune) + orientational (delayed, purely anisotropic). - m = new Material { Name = "CS2", Index = new SellmeierIndex(new[] { 1.50387 }, new[] { 0.03049 }, 1.0, "Kedenburg et al., Opt. Mater. Express 2, 1588 (2012), 20 C"), - N2 = CS2N2TotalPerW, RamanFraction = CS2OrientationalFraction, Raman = new OrientationalResponse(CS2RiseS, CS2DecayS) }; - m.Notes = "liquid CS2 at 20 C; index Kedenburg 2012 (fit valid 0.5-1.6 um, extrapolated outside); n2 = 3.0e-18 m^2/W total (ps pulses) of which 10 % electronic " - + "(3e-19, the fs value of Liu 2013 / Ganeev 2004) and 90 % orientational, h(t) = (exp(-t/1.6 ps) - exp(-t/0.2 ps)) / 1.4 ps; the split and the time constants are UNVERIFIED typical values (spread of the literature is a factor 2-3): override n2 / ramanFraction / raman"; - break; - // thin-film materials (ion-beam-sputtered, approximate Cauchy fits; override for real designs) - case "ta2o5": m = Film("Ta2O5", 2.0500, 0.0200, 0.0012); break; - case "tio2": m = Film("TiO2", 2.2000, 0.0550, 0.0030); break; - case "hfo2": m = Film("HfO2", 1.9500, 0.0160, 0.0); break; - case "nb2o5": m = Film("Nb2O5", 2.1800, 0.0400, 0.0); break; - case "al2o3_film": m = Film("Al2O3_film", 1.6200, 0.0080, 0.0); break; - case "sio2_film": m = Film("SiO2_film", 1.4500, 0.0036, 0.0); break; - case "he": case "ne": case "ar": case "kr": case "xe": case "n2": case "o2": case "co2": case "h2": - m = Gas(c, gasPressureBar, gasTemperatureK); - break; - default: - throw new ModelException("Unknown material \"" + name + "\". Built-in materials: " + string.Join(", ", BuiltInNames) + ". Define it in the system file's \"materials\" section (e.g. as an index table)."); - } + if (e.Kind == "gas") m = string.Equals(e.Id, "Air", StringComparison.OrdinalIgnoreCase) ? Air(e) : Gas(e.Id, gasPressureBar, gasTemperatureK); + else m = Solid(e); foreach (var p in new[] { "index", "n2", "raman" }) if (!m.Provenance.ContainsKey(p)) m.Provenance[p] = (p == "index" ? m.Index.Description : p == "n2" ? m.N2.ToString("G3", System.Globalization.CultureInfo.InvariantCulture) + " m^2/W" : (m.Raman?.Description ?? "none"), ModelSource.MaterialDefault, "built-in library"); return m; } + /// A solid or liquid of the catalogue: index (air-referenced where the data are), n2, delayed response, cubic anisotropy. + private static Material Solid(CatalogueEntry e) + { + var qi = e.Q("index"); + var rel = CatalogueModels.Index(qi); + var m = new Material { Name = e.Id, Notes = e.Notes }; + if (CatalogueModels.IsAirReferenced(qi)) { m.Index = new AirReferencedIndex(rel, new CiddorAirIndex()); m.IndexReference = "air"; } + else m.Index = rel; + m.Used["index"] = qi; + var n2 = e.Q("n2"); + m.N2 = n2.Number("valueM2PerW"); m.Used["n2"] = n2; + var rq = e.TryQ("raman"); + if (rq != null) + { + var re = Catalogue.Get(rq.Text("ref"), "raman"); + m.Raman = CatalogueModels.Raman(re); + var own = e.TryQ("ramanFraction"); + m.RamanFraction = own != null ? own.Number("value") : CatalogueModels.RamanFractionOf(re); + m.Used["raman"] = re.Q("response"); + var fq = own ?? re.TryQ("ramanFraction"); + if (fq != null) m.Used["ramanFraction"] = fq; + } + var ka = e.TryQ("kerrAnisotropy"); + if (ka != null) { m.KerrAnisotropy = ka.Number("value"); m.Used["kerrAnisotropy"] = ka; } + return m; + } + + /// Dry air at 20 C, 101325 Pa (Ciddor 1996): electronic n2 plus the rotational Raman response of its N2 and O2. + private static Material Air(CatalogueEntry e) + { + var m = new Material { Name = "Air", Index = new CiddorAirIndex(), IsGas = true, N2 = GasN2PerBar["Air"] * 1.01325 * 273.0 / 293.15, + Notes = "Ciddor 1996 at 20 C, 101325 Pa, 0 % RH, 450 ppm CO2; electronic n2 + rotational Raman of N2 and O2", + Rotational = RotationalRamanResponse.For(GasData.AirComposition, 1.01325, 293.15), GasSpecies = "Air", GasPressureBar = 1.01325, GasTemperatureK = 293.15 }; + m.Used["index"] = e.Q("index"); m.Used["n2"] = e.Q("n2Electronic"); + foreach (var (sp, _) in GasData.AirComposition) UseRotation(m, sp); + return m; + } + + private static void UseRotation(Material m, string species) + { + var g = GasData.Get(species); + if (g == null || !g.Molecular) return; + m.Used["rotation:" + species] = g.Entry.Q("rotation"); + m.Used["dephasing:" + species] = g.Entry.Q("dephasing"); + } + /// The Raman fraction that belongs to a built-in Raman model kind (NaN: none implied). public static double DefaultRamanFraction(string kind) { switch ((kind ?? "").ToLowerInvariant()) { - case "silica": case "blowwood": return 0.18; + case "silica": case "blowwood": return CatalogueModels.RamanFractionOf(Catalogue.Get("blowWood", "raman")); case "hollenbeck": return HollenbeckCantrellRamanResponse.DefaultFraction; case "linagrawal": return LinAgrawalRamanResponse.DefaultFraction; default: return double.NaN; } } - /// Liquid CS2 nonlinearity (UNVERIFIED typical values, see Material.Notes): total n2 for ps pulses, delayed (orientational) share, rise and decay times. - /// BaF2 Kerr data (UNVERIFIED typical values, see Material.Notes): n2 along [100] and cubic anisotropy sigma. - public const double BaF2N2PerW = 2.0e-20, BaF2Sigma = -1.2; - - public const double CS2N2TotalPerW = 3.0e-18, CS2OrientationalFraction = 0.9, CS2RiseS = 0.2e-12, CS2DecayS = 1.6e-12; - - private static Material Glass(string name, IRefractiveIndex relIndex, IRefractiveIndex air, double n2, double fR, IDelayedResponse raman) => - new Material { Name = name, Index = new AirReferencedIndex(relIndex, air), IndexReference = "air", N2 = n2, RamanFraction = fR, Raman = raman }; + /// BaF2 Kerr data (catalogue entry BaF2; UNVERIFIED typical values): n2 along [100] and cubic anisotropy sigma. + public static double BaF2N2PerW => Catalogue.Get("BaF2", "solid").Q("n2").Number("valueM2PerW"); + public static double BaF2Sigma => Catalogue.Get("BaF2", "solid").Q("kerrAnisotropy").Number("value"); - private static Material Film(string name, double a, double b, double c) => - new Material { Name = name, Index = new CauchyIndex(new[] { a, b, c }), N2 = 0, Notes = "thin-film Cauchy approximation" }; + /// Liquid CS2 nonlinearity (catalogue entry CS2; UNVERIFIED typical values): total n2 for ps pulses, delayed (orientational) share, rise and decay times. + public static double CS2N2TotalPerW => Catalogue.Get("CS2", "solid").Q("n2").Number("valueM2PerW"); + public static double CS2OrientationalFraction => Catalogue.Get("CS2", "solid").Q("ramanFraction").Number("value"); + public static double CS2RiseS => Catalogue.Get("orientational", "raman").Q("response").Number("riseS"); + public static double CS2DecayS => Catalogue.Get("orientational", "raman").Q("response").Number("decayS"); /// A gas at pressure and temperature: index (Borzsonyi fits, or dispersion formulas for O2, CO2, H2), electronic /// n2 scaled with density, and for molecules the rotational Raman response (docs/reference/gases.md). @@ -192,7 +120,7 @@ public static Material Gas(string species, double pressureBar, double temperatur string sp = g?.Species ?? species; if (string.Equals(sp, "air", StringComparison.OrdinalIgnoreCase)) { - var am = BuiltIn("Air"); + var am = BuiltIn("Air"); // keeps its catalogue quantities (Used); only the state changes below am.Index = new CiddorAirIndex(temperatureK - 273.15, pressureBar * 1e5, 0, 450); double f = pressureBar / 1.01325 * 293.15 / temperatureK; am.N2 *= f; am.Rotational = RotationalRamanResponse.For(GasData.AirComposition, pressureBar, temperatureK); @@ -204,12 +132,15 @@ public static Material Gas(string species, double pressureBar, double temperatur else { var gi = new GasIndex(sp, pressureBar, temperatureK); idx = gi; density = gi.DensityFactor; } double n2 = GasN2PerBar.TryGetValue(sp, out var v) ? v * density : 0.0; var rot = g != null && g.Molecular ? RotationalRamanResponse.For(new[] { (sp, 1.0) }, pressureBar, temperatureK) : null; - return new Material + var mat = new Material { Name = sp + (pressureBar != 1.0 ? "@" + pressureBar.ToString("G4", System.Globalization.CultureInfo.InvariantCulture) + "bar" : ""), Index = idx, IsGas = true, N2 = n2, Rotational = rot, GasSpecies = sp, GasPressureBar = pressureBar, GasTemperatureK = temperatureK, Notes = "electronic n2 scaled with density from " + (GasN2PerBar.TryGetValue(sp, out var b) ? b.ToString("G3", System.Globalization.CultureInfo.InvariantCulture) : "?") + " m^2/W at 1 bar, 273 K" + (rot != null ? "; plus rotational Raman (adiabatic n2 " + rot.AdiabaticN2.ToString("G3", System.Globalization.CultureInfo.InvariantCulture) + " m^2/W)" : "") }; + var ge = Catalogue.Find(sp, "gas"); + if (ge != null) { mat.Used["index"] = ge.Q("index"); if (ge.Has("n2Electronic")) mat.Used["n2"] = ge.Q("n2Electronic"); UseRotation(mat, sp); } + return mat; } /// @@ -259,11 +190,13 @@ public static void Apply(Material m, JsonObject o, ModelRegistry reg, ModelSourc } m.Index = idx; m.Provenance["index"] = (idx.Description, source, origin); + m.Used.Remove("index"); } - if (o.Has("kerrAnisotropy")) { m.KerrAnisotropy = o.GetDouble("kerrAnisotropy", 0); m.Provenance["kerrAnisotropy"] = (m.KerrAnisotropy.ToString("G4", System.Globalization.CultureInfo.InvariantCulture), source, origin); } - if (o.Has("n2")) { m.N2 = o.GetDouble("n2", 0); m.Provenance["n2"] = (m.N2.ToString("G4", System.Globalization.CultureInfo.InvariantCulture) + " m^2/W", source, origin); } + if (o.Has("kerrAnisotropy")) { m.Used.Remove("kerrAnisotropy"); m.KerrAnisotropy = o.GetDouble("kerrAnisotropy", 0); m.Provenance["kerrAnisotropy"] = (m.KerrAnisotropy.ToString("G4", System.Globalization.CultureInfo.InvariantCulture), source, origin); } + if (o.Has("n2")) { m.Used.Remove("n2"); m.N2 = o.GetDouble("n2", 0); m.Provenance["n2"] = (m.N2.ToString("G4", System.Globalization.CultureInfo.InvariantCulture) + " m^2/W", source, origin); } if (o.TryGet("rotationalRaman", out var rr) && !rr.IsNull) { + foreach (var k in m.Used.Keys.Where(k => k.StartsWith("rotation:") || k.StartsWith("dephasing:")).ToList()) m.Used.Remove(k); if (rr.Kind == JsonKind.Bool && !rr.AsBool()) m.Rotational = null; else if (rr.Kind == JsonKind.Object) { @@ -274,9 +207,10 @@ public static void Apply(Material m, JsonObject o, ModelRegistry reg, ModelSourc } m.Provenance["rotationalRaman"] = (m.Rotational?.Description ?? "off", source, origin); } - if (o.Has("ramanFraction")) m.RamanFraction = o.GetDouble("ramanFraction", 0); + if (o.Has("ramanFraction")) { m.RamanFraction = o.GetDouble("ramanFraction", 0); m.Used.Remove("ramanFraction"); } if (o.Has("raman")) { + m.Used.Remove("raman"); if (!o.Has("ramanFraction")) m.Used.Remove("ramanFraction"); var rs = ModelSpec.FromJson(o["raman"], source, origin + ".raman"); m.Raman = reg.Create("delayedResponse", rs); if (rs.Json.Has("fR")) m.RamanFraction = rs.Json.GetDouble("fR", m.RamanFraction); diff --git a/src/UltrafastEngine.Core/Materials/RamanModels.cs b/src/UltrafastEngine.Core/Materials/RamanModels.cs index 7d593a5..971a173 100644 --- a/src/UltrafastEngine.Core/Materials/RamanModels.cs +++ b/src/UltrafastEngine.Core/Materials/RamanModels.cs @@ -26,10 +26,16 @@ public double[] EvaluateAnisotropic(double[] t) return h; } public double AnisotropicFraction => (_fb + _fc) / (_fa + _fb + _fc); - public const double DefaultFraction = 0.245; + /// f_R of the model (catalogue entry "linAgrawal"). + public static double DefaultFraction => CatalogueModels.RamanFractionOf(Catalogue.Get("linAgrawal", "raman")); private readonly double _t1, _t2, _tb, _fa, _fb, _fc; - public LinAgrawalRamanResponse(double tau1 = 12.2e-15, double tau2 = 32e-15, double taub = 96e-15, double fa = 0.75, double fb = 0.21, double fc = 0.04) - { _t1 = tau1; _t2 = tau2; _tb = taub; _fa = fa; _fb = fb; _fc = fc; } + /// Parameters not given (NaN) come from the catalogue entry "linAgrawal" (tau1S, tau2S, taubS, fa, fb, fc). + public LinAgrawalRamanResponse(double tau1 = double.NaN, double tau2 = double.NaN, double taub = double.NaN, double fa = double.NaN, double fb = double.NaN, double fc = double.NaN) + { + var q = Catalogue.Get("linAgrawal", "raman").Q("response"); + _t1 = double.IsNaN(tau1) ? q.Number("tau1S") : tau1; _t2 = double.IsNaN(tau2) ? q.Number("tau2S") : tau2; _tb = double.IsNaN(taub) ? q.Number("taubS") : taub; + _fa = double.IsNaN(fa) ? q.Number("fa") : fa; _fb = double.IsNaN(fb) ? q.Number("fb") : fb; _fc = double.IsNaN(fc) ? q.Number("fc") : fc; + } public string Description => string.Format(CultureInfo.InvariantCulture, "Lin-Agrawal silica Raman (damped oscillator {0:G3}/{1:G3} fs + boson peak {2:G3} fs; fa {3}, fb {4}, fc {5})", _t1 * 1e15, _t2 * 1e15, _tb * 1e15, _fa, _fb, _fc); public double[] Evaluate(double[] t) { @@ -57,19 +63,25 @@ public double[] Evaluate(double[] t) /// public sealed class HollenbeckCantrellRamanResponse : IDelayedResponse { - public const double DefaultFraction = 0.18; - // component positions (cm^-1), peak intensities, Gaussian FWHM (cm^-1), Lorentzian FWHM (cm^-1): Table 1 of the paper - private static readonly double[] Pos = { 56.25, 100.00, 231.25, 362.50, 463.00, 497.00, 611.50, 691.67, 793.67, 835.50, 930.00, 1080.00, 1215.00 }; - private static readonly double[] Amp = { 1.00, 11.40, 36.67, 67.67, 74.00, 4.50, 6.80, 4.60, 4.20, 4.50, 2.70, 3.10, 3.00 }; - private static readonly double[] Gfw = { 52.10, 110.42, 175.00, 162.50, 135.33, 24.50, 41.50, 155.00, 59.50, 64.30, 150.00, 91.00, 160.00 }; - private static readonly double[] Lfw = { 17.37, 38.81, 58.33, 54.17, 45.11, 8.17, 13.83, 51.67, 19.83, 21.43, 50.00, 30.33, 53.33 }; - private static double _norm = double.NaN; + /// f_R of the model (catalogue entry "hollenbeck"). + public static double DefaultFraction => CatalogueModels.RamanFractionOf(Catalogue.Get("hollenbeck", "raman")); + + // component positions (cm^-1), peak intensities, Gaussian FWHM (cm^-1), Lorentzian FWHM (cm^-1): Table 1 of the paper, from the + // catalogue entry "hollenbeck" (with the unit-integral normalisation, computed once per loaded catalogue) + private sealed class Table { public double[] Pos, Amp, Gfw, Lfw; public double Norm = double.NaN; } + private static Table T => Catalogue.Cached("hollenbeck.table", () => + { + var q = Catalogue.Get("hollenbeck", "raman").Q("response"); + return new Table { Pos = q.Array("modePositionsPerCm"), Amp = q.Array("peakIntensities"), Gfw = q.Array("gaussianFwhmPerCm"), Lfw = q.Array("lorentzianFwhmPerCm") }; + }); public string Description => "Hollenbeck-Cantrell silica Raman (13-mode intermediate-broadening model)"; - private static double Raw(double t) + private static double Raw(double t) => Raw(T, t); + private static double Raw(Table tb, double t) { if (t < 0) return 0; + double[] Pos = tb.Pos, Amp = tb.Amp, Gfw = tb.Gfw, Lfw = tb.Lfw; const double cCm = 2.99792458e10; // cm/s double s = 0; for (int i = 0; i < Pos.Length; i++) @@ -83,19 +95,21 @@ private static double Raw(double t) private static double Norm() { - if (!double.IsNaN(_norm)) return _norm; + var tb = T; + if (!double.IsNaN(tb.Norm)) return tb.Norm; // unit integral, evaluated on a fine grid (the response has decayed after ~2 ps) const int n = 400000; const double tMax = 4e-12; double dt = tMax / n, s = 0; - for (int i = 0; i <= n; i++) s += (i == 0 || i == n ? 0.5 : 1.0) * Raw(i * dt); - _norm = 1.0 / (s * dt); - return _norm; + for (int i = 0; i <= n; i++) s += (i == 0 || i == n ? 0.5 : 1.0) * Raw(tb, i * dt); + tb.Norm = 1.0 / (s * dt); + return tb.Norm; } public double[] Evaluate(double[] t) { double k = Norm(); + var tb = T; var h = new double[t.Length]; - for (int i = 0; i < t.Length; i++) h[i] = k * Raw(t[i]); + for (int i = 0; i < t.Length; i++) h[i] = k * Raw(tb, t[i]); return h; } } diff --git a/src/UltrafastEngine.Core/Models/Interfaces.cs b/src/UltrafastEngine.Core/Models/Interfaces.cs index 62d7fbe..f2bace7 100644 --- a/src/UltrafastEngine.Core/Models/Interfaces.cs +++ b/src/UltrafastEngine.Core/Models/Interfaces.cs @@ -93,6 +93,22 @@ public interface ITimeDomainResponse void Evaluate(ResponseContext ctx, double[] dnRe, double[] dnIm); } + /// + /// A time-domain response with a part whose index change has a frequency dependence of its own, such as the free-electron (Drude) + /// term Δn(ω) ∝ 1/(ω (ω + iν)). writes that part, evaluated at , to + /// dispRe / dispIm and the rest to dnRe / dnIm; the propagator multiplies the spectrum of + /// (dispRe + i dispIm) A by at every frequency instead of using the carrier value. Responses that do not + /// implement it are used as before. User models need not implement it. + /// + public interface IDispersiveResponse : ITimeDomainResponse + { + /// False switches the dispersive part off: is used (everything at the carrier). + bool FrequencyResolved { get; } + void EvaluateSplit(ResponseContext ctx, double[] dnRe, double[] dnIm, double[] dispRe, double[] dispIm); + /// Complex factor that takes the dispersive index change from the reference frequency to (rad/s, > 0): 1 at the reference. + System.Numerics.Complex Spectral(double omegaAbs, double omegaRef); + } + /// A spectral filter: power transmission T(omega) in [0,1] and spectral phase phi(omega) (rad). /// Phase may contain a linear part -- the engine removes the carrier group delay and reports it. public interface ISpectralFilter diff --git a/src/UltrafastEngine.Core/Models/ModelRegistry.cs b/src/UltrafastEngine.Core/Models/ModelRegistry.cs index f524e78..30b7b19 100644 --- a/src/UltrafastEngine.Core/Models/ModelRegistry.cs +++ b/src/UltrafastEngine.Core/Models/ModelRegistry.cs @@ -186,18 +186,18 @@ IDelayedResponse Split(IDelayedResponse h, ModelSpec s, double def) if (double.IsNaN(b)) return h; // linAgrawal without the key keeps its own fa / fb / fc split return b != 0 && h != null ? new AnisotropicSplitResponse(h, b) : h; } - r.Register("delayedResponse", "silica", (s, _) => Split(new DampedOscillatorResponse(12.2e-15, 32e-15), s, 0.25)); - r.Register("delayedResponse", "blowWood", (s, _) => Split(new DampedOscillatorResponse(12.2e-15, 32e-15), s, 0.25)); + r.Register("delayedResponse", "silica", (s, _) => Split(new DampedOscillatorResponse(CatalogueModels.RamanDefaults.BlowWoodTau1S, CatalogueModels.RamanDefaults.BlowWoodTau2S), s, CatalogueModels.RamanDefaults.BlowWoodAnisotropicFraction)); + r.Register("delayedResponse", "blowWood", (s, _) => Split(new DampedOscillatorResponse(CatalogueModels.RamanDefaults.BlowWoodTau1S, CatalogueModels.RamanDefaults.BlowWoodTau2S), s, CatalogueModels.RamanDefaults.BlowWoodAnisotropicFraction)); r.Register("delayedResponse", "linAgrawal", (s, _) => { - var la = new LinAgrawalRamanResponse(s.GetDouble("tau1", 12.2e-15), s.GetDouble("tau2", 32e-15), s.GetDouble("taub", 96e-15), s.GetDouble("fa", 0.75), s.GetDouble("fb", 0.21), s.GetDouble("fc", 0.04)); + var la = new LinAgrawalRamanResponse(s.GetDouble("tau1", double.NaN), s.GetDouble("tau2", double.NaN), s.GetDouble("taub", double.NaN), s.GetDouble("fa", double.NaN), s.GetDouble("fb", double.NaN), s.GetDouble("fc", double.NaN)); // not given: the catalogue entry linAgrawal // an explicit anisotropicFraction (even 0 = isotropic) replaces the model's own fa / fb / fc split return s.Json.Has("anisotropicFraction") ? (IDelayedResponse)new AnisotropicSplitResponse(la, s.GetDouble("anisotropicFraction", 0.25)) : la; }); - r.Register("delayedResponse", "hollenbeck", (s, _) => Split(new HollenbeckCantrellRamanResponse(), s, 0.25)); + r.Register("delayedResponse", "hollenbeck", (s, _) => Split(new HollenbeckCantrellRamanResponse(), s, CatalogueModels.RamanDefaults.HollenbeckAnisotropicFraction)); r.Register("delayedResponse", "dampedOscillator", (s, _) => Split(new DampedOscillatorResponse(s.GetRequired("tau1"), s.GetRequired("tau2")), s, 0)); r.Register("delayedResponse", "orientational", (s, _) => - new OrientationalResponse(s.GetDouble("riseS", 0.2e-12), s.GetDouble("decayS", 1.6e-12), s.GetDouble("anisotropicFraction", 1.5))); + new OrientationalResponse(s.GetDouble("riseS", CatalogueModels.RamanDefaults.OrientationalRiseS), s.GetDouble("decayS", CatalogueModels.RamanDefaults.OrientationalDecayS), s.GetDouble("anisotropicFraction", CatalogueModels.RamanDefaults.OrientationalAnisotropicFraction))); r.Register("delayedResponse", "expression", (s, _) => { var e = CompiledExpression.Compile(s.Json.GetRequiredString("expr", s.Origin), new[] { "t" }, s.Params()); diff --git a/src/UltrafastEngine.Core/Models/ModelSpec.cs b/src/UltrafastEngine.Core/Models/ModelSpec.cs index 488a5e0..a75fa3c 100644 --- a/src/UltrafastEngine.Core/Models/ModelSpec.cs +++ b/src/UltrafastEngine.Core/Models/ModelSpec.cs @@ -103,6 +103,10 @@ public sealed class ProvenanceEntry public string Origin { get; set; } /// Lower-priority candidates that were shadowed (so a user can see that an override took effect). public List Shadowed { get; } = new List(); + /// The catalogue quantity the value comes from (null: not from the material catalogue), with its status and source. + public Materials.CatalogueQuantity Catalogue { get; set; } + /// Name of the material the catalogue value belongs to (set with ). + public string Material { get; set; } public JsonObject ToJson() { @@ -110,6 +114,7 @@ public JsonObject ToJson() .Set("target", Target).Set("property", Property).Set("value", Value) .Set("source", Source.ToString()).Set("origin", Origin); if (Shadowed.Count > 0) o.Set("shadowed", JsonValue.FromArray(Shadowed)); + if (Catalogue != null) o.Set("catalogueStatus", Catalogue.Status.ToString().ToLowerInvariant()).Set("catalogueSource", Catalogue.Source).Set("catalogueQuantity", Catalogue.Path); return o; } } @@ -128,6 +133,28 @@ public ProvenanceEntry Record(string target, string property, string value, Mode } public JsonArray ToJson() => new JsonArray(_entries.Select(e => (JsonValue)e.ToJson())); + + private readonly List _catalogueUses = new List(); + private readonly HashSet _catalogueSeen = new HashSet(StringComparer.Ordinal); + /// Catalogue values the run used that are not verified (status approximate or unverified) or come from a user file, once each. + public IReadOnlyList UnverifiedInputs => _catalogueUses; + + /// Record that uses a catalogue quantity. A value that is not verified, or comes from a user file, is listed once + /// in and, through , in the run notes, so a quantitative result is never silently built on it. + public void UseCatalogue(string target, string property, Materials.CatalogueQuantity q, string materialName, List notes) + { + var e = Record(target, property + " (catalogue)", q.Path, ModelSource.MaterialDefault, "catalogue " + q.Origin); + e.Catalogue = q; e.Material = materialName; + if (q.Status == Materials.CatalogueStatus.Verified && !q.IsUser) return; + if (!_catalogueSeen.Add(q.Path + "|" + q.Origin)) return; + _catalogueUses.Add(e); + notes?.Add((q.Status == Materials.CatalogueStatus.Verified ? "user catalogue value used: " : q.Status == Materials.CatalogueStatus.Approximate ? "APPROXIMATE catalogue value used: " : "UNVERIFIED catalogue value used: ") + + materialName + "." + property + " [catalogue " + q.Path + "] -- " + q.Citation() + ". Override it (docs/reference/materials.md) or check the source before relying on a quantitative result."); + } + + public JsonArray UnverifiedInputsJson() => new JsonArray(_catalogueUses.Select(e => (JsonValue)new JsonObject() + .Set("quantity", e.Catalogue.Path).Set("material", e.Material).Set("property", e.Property.Replace(" (catalogue)", "")).Set("target", e.Target).Set("status", e.Catalogue.Status.ToString().ToLowerInvariant()).Set("source", e.Catalogue.Source) + .Set("uncertainty", double.IsNaN(e.Catalogue.RelativeUncertainty) ? (JsonValue)e.Catalogue.Uncertainty : e.Catalogue.RelativeUncertainty).Set("origin", e.Catalogue.Origin))); } /// diff --git a/src/UltrafastEngine.Core/Optics/BeamTracking.cs b/src/UltrafastEngine.Core/Optics/BeamTracking.cs index 5c76167..967207b 100644 --- a/src/UltrafastEngine.Core/Optics/BeamTracking.cs +++ b/src/UltrafastEngine.Core/Optics/BeamTracking.cs @@ -154,6 +154,68 @@ public void Propagate(double lengthM, double n, Func nOf = null) Ex += lengthM / n * Fx; Ey += lengthM / n * Fy; } + /// + /// Reduced ABCD matrix { A, B, C, D } (row-major, as ) of a graded-index duct of physical length + /// , n(r) = n0 (1 - g² r² / 2) with g² = (focusing for g² > 0, defocusing for g² < 0): + /// A = D = cos(gL), B = sin(gL) / (n0 g), C = -n0 g sin(gL) (Saleh & Teich, Fundamentals of Photonics, ch. 1, GRIN media; reduced + /// slope n θ). For g L → 0 it tends to a gap of reduced length L / n0 followed by a thin lens of power n0 g² L. + /// + public static double[] DuctMatrix(double lengthM, double n0, double g2PerM2) + { + double x = g2PerM2 * lengthM * lengthM; // (gL)², signed + if (Math.Abs(x) < 1e-8) // series: cos = 1 - x/2 + x²/24, sin(gL)/(gL) = 1 - x/6 + x²/120 + { + double sinc = 1 - x / 6 + x * x / 120, cs = 1 - x / 2 + x * x / 24; + return new[] { cs, lengthM / n0 * sinc, -n0 * g2PerM2 * lengthM * sinc, cs }; + } + if (x > 0) + { + double g = Math.Sqrt(g2PerM2), s = Math.Sin(g * lengthM), c = Math.Cos(g * lengthM); + return new[] { c, s / (n0 * g), -n0 * g * s, c }; + } + double h = Math.Sqrt(-g2PerM2), sh = Math.Sinh(h * lengthM), ch = Math.Cosh(h * lengthM); + return new[] { ch, sh / (n0 * h), n0 * h * sh, ch }; + } + + /// + /// The thin-lens-equivalent power 1/f_thin = n0 g² L (1/m) of a uniform duct of length whose effective focal + /// length -1/C = 1 / (n0 g sin gL) (focusing; 1 / (n0 h sinh hL) for defocusing, f < 0) equals . + /// The search is limited to a quarter pitch (gL = π/2, where the matrix element A reaches 0 and a collimated beam comes to a focus inside the medium): a shorter focal length sets and gL = π/2 is used (a duct up to gL = 2.03 would still have a monotone growth of n0 g sin gL up to its maximum, but its output is no longer a lens in the usual sense). + /// + public static double ThinPowerForEffectiveFocalLength(double lengthM, double n0, double effectiveFocalLengthM, out bool capped) + { + capped = false; + if (double.IsInfinity(effectiveFocalLengthM) || effectiveFocalLengthM == 0 || double.IsNaN(effectiveFocalLengthM)) return 0; + double target = 1 / Math.Abs(effectiveFocalLengthM); // n0 g sin(gL) = (n0 / L) u sin(u), u = gL + double sign = effectiveFocalLengthM > 0 ? 1 : -1, lo = 0, hi; + Func f; + if (sign > 0) + { + f = u => n0 / lengthM * u * Math.Sin(u); + hi = Math.PI / 2; + if (f(hi) < target) { capped = true; return n0 * hi * hi / lengthM; } + } + else + { + f = u => n0 / lengthM * u * Math.Sinh(u); + hi = 1; while (f(hi) < target && hi < 700) hi *= 2; + } + for (int i = 0; i < 200; i++) { double mid = 0.5 * (lo + hi); if (f(mid) < target) lo = mid; else hi = mid; } + double u0 = 0.5 * (lo + hi); + return sign * n0 * u0 * u0 / lengthM; + } + + /// Propagation through a graded-index duct (a distributed lens such as a thermal lens): see . + /// The same duct acts on both axes unless the powers differ (, ). + public void Duct(double lengthM, double n0, double g2xPerM2, double g2yPerM2) + { + if (!(n0 > 0)) n0 = 1; + EnterMedium(n0, null); + var mx = DuctMatrix(lengthM, n0, g2xPerM2); var my = DuctMatrix(lengthM, n0, g2yPerM2); + Apply(mx[0], mx[1], mx[2], mx[3], my[0], my[1], my[2], my[3]); + MediumIndex = n0; PathM += lengthM; + } + /// Index of the present medium for ray j. private double RayIndex(int j) => _mediumOf != null ? _mediumOf(RayOmega(j)) : MediumIndex; diff --git a/src/UltrafastEngine.Core/Optics/ModulatorElement.cs b/src/UltrafastEngine.Core/Optics/ModulatorElement.cs index dad4aca..8b5b029 100644 --- a/src/UltrafastEngine.Core/Optics/ModulatorElement.cs +++ b/src/UltrafastEngine.Core/Optics/ModulatorElement.cs @@ -145,12 +145,19 @@ public Complex[] CellsAt(CartesianBasis b, double omega) return t; } + private static double Frac(double v) => v - Math.Floor(v); + public PulseField Execute(ActiveContext ctx, PulseField p) { if (p.Profile == null) { ctx.Result.Warnings.Add("Element '" + ctx.Element.Id + "': a modulator acts on a carried transverse profile; there is none here, so it was ignored (give the element before it a \"transverse\" model)."); return p; } if (!(p.Profile.Basis is CartesianBasis B)) throw new InvalidOperationException("element '" + ctx.Element.Id + "': a modulator needs a cartesian transverse grid (\"model\": \"cartesian\")."); double cellW = Math.Min(PitchXM, PitchYM), dxs = Math.Min(B.Dx, B.Ny > 1 ? B.Dy : double.MaxValue); if (cellW < 2 * dxs) ctx.Result.Warnings.Add(string.Format(CultureInfo.InvariantCulture, "Element '{0}': the device cell ({1:G3} m) is under two grid steps ({2:G3} m): the cell structure (replica orders, gaps) is under-resolved; use at least 2 grid cells per device cell, or 4 to show the fill factor.", ctx.Element.Id, cellW, dxs)); + // device cell edges on the simulation grid points: the grid has a sample at x = 0, the cell centres of an even device are half a pitch from the axis, so with + // pitch = grid step every simulation cell averages two device cells (a blaze of period P keeps cos^2(pi / P) of its light); an offset of half a pitch aligns them + bool straddleX = Math.Abs(PitchXM / B.Dx - 1) < 1e-3 && Math.Abs(Frac(Nx / 2.0 - OffsetXM / PitchXM) - 0.5) > 0.05 && RotationRad == 0; + bool straddleY = B.Ny > 1 && Math.Abs(PitchYM / B.Dy - 1) < 1e-3 && Math.Abs(Frac(Ny / 2.0 - OffsetYM / PitchYM) - 0.5) > 0.05 && RotationRad == 0; + if (straddleX || straddleY) ctx.Result.Warnings.Add(string.Format(CultureInfo.InvariantCulture, "Element '{0}': the device pitch equals the grid step but the device cell edges fall on the grid points, so each simulation cell averages two device cells along {1} (a blazed grating of P cells per period keeps only cos^2(pi/P) of its light). Shift the device by half a pitch (offsetXM / offsetYM = pitch / 2) to put the cell centres on the grid points, or use a grid step of pitch / 2 or less.", ctx.Element.Id, straddleX && straddleY ? "x and y" : straddleX ? "x" : "y")); int nf = p.Profile.Omega.Length, cached = -1; Complex[] cur = null; // aliasing guard (the interpolation error below cannot see a phase step near a multiple of 2 pi per stored frequency) double maxPhase = _phase.Max() - _phase.Min(), dS = 0; // the phase range: a uniform offset is a piston, not a chromatic structure diff --git a/src/UltrafastEngine.Core/Physics/DispersiveResponses.cs b/src/UltrafastEngine.Core/Physics/DispersiveResponses.cs new file mode 100644 index 0000000..ece6d46 --- /dev/null +++ b/src/UltrafastEngine.Core/Physics/DispersiveResponses.cs @@ -0,0 +1,72 @@ +// DispersiveResponses.cs -- the frequency-resolved part of time-domain responses (the Drude term of a plasma). +// +// A time-domain response writes Delta n(t) at the carrier; the propagators then multiply the product Delta n(t) A(t) by i k0 in the +// frequency domain. A response that implements IDispersiveResponse has a part, Delta n_d(t), whose index change depends on the frequency +// of the light it acts on. Here that part is applied as +// dA/dz (omega) += i (omega / c) R(omega) FFT[ Delta n_d(t) A(t) ](omega), R = Delta n_d(omega) / Delta n_d(omega_ref), +// for omega > 0 (no negative frequencies), the first-order expansion in the density with the density varying slowly against the optical cycle. +// +// Classes (internal): DispersiveTerm (a response with its Delta n_d(t) arrays), DispersiveResponses +// Evaluate(ctx, responses, uRe, uIm, lockEach) -> terms or null evaluates all responses; ordinary ones add into uRe / uIm +// AddRate(terms, a, omegaAbs, rate, offset, firstBin, wCut) adds the dispersive rate of the field a (time domain) to rate[offset + j] +// AddToTime(terms, a, k0, n) adds i k0 Delta n_d a into a time-domain array (for the step-size estimate) +using System.Collections.Generic; +using System.Numerics; +using UltrafastEngine.Models; +using UltrafastEngine.Numerics; + +namespace UltrafastEngine.Physics +{ + internal sealed class DispersiveTerm + { + public IDispersiveResponse Response; + public double[] Re, Im; + public double OmegaRef; + } + + internal static class DispersiveResponses + { + public static List Evaluate(ResponseContext ctx, IList responses, double[] uRe, double[] uIm, bool lockEach) + { + int n = ctx.N; var re = new double[n]; var im = new double[n]; + List terms = null; + foreach (var resp in responses) + { + System.Array.Clear(re, 0, n); System.Array.Clear(im, 0, n); + if (resp is IDispersiveResponse d && d.FrequencyResolved) + { + var t = new DispersiveTerm { Response = d, Re = new double[n], Im = new double[n], OmegaRef = ctx.Omega0 }; + if (lockEach) lock (resp) d.EvaluateSplit(ctx, re, im, t.Re, t.Im); else d.EvaluateSplit(ctx, re, im, t.Re, t.Im); + (terms ?? (terms = new List())).Add(t); + } + else if (lockEach) lock (resp) resp.Evaluate(ctx, re, im); + else resp.Evaluate(ctx, re, im); + for (int i = 0; i < n; i++) { uRe[i] += re[i]; uIm[i] += im[i]; } + } + return terms; + } + + public static void AddRate(List terms, Complex[] a, double[] omegaAbs, Complex[] rate, int offset, int firstBin, double wCut) + { + int n = a.Length; double c = PhysicalConstants.SpeedOfLightMPerS; + var prod = new Complex[n]; + foreach (var t in terms) + { + for (int i = 0; i < n; i++) prod[i] = new Complex(t.Re[i], t.Im[i]) * a[i]; + var f = SpectralTransform.TimeToFreq(prod); + for (int j = firstBin; j < n; j++) + { + double w = omegaAbs[j]; + if (!(w > 0) || w > wCut) continue; + rate[offset + j] += Complex.ImaginaryOne * (w / c) * t.Response.Spectral(w, t.OmegaRef) * f[j]; + } + } + } + + public static void AddToTime(List terms, Complex[] a, double k0, Complex[] target) + { + foreach (var t in terms) + for (int i = 0; i < a.Length; i++) target[i] += Complex.ImaginaryOne * k0 * new Complex(t.Re[i], t.Im[i]) * a[i]; + } + } +} diff --git a/src/UltrafastEngine.Core/Physics/Ionization.cs b/src/UltrafastEngine.Core/Physics/Ionization.cs index 8f419be..7f227eb 100644 --- a/src/UltrafastEngine.Core/Physics/Ionization.cs +++ b/src/UltrafastEngine.Core/Physics/Ionization.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Numerics; using UltrafastEngine.Materials; using UltrafastEngine.Models; using UltrafastEngine.Numerics; @@ -185,6 +186,7 @@ public double Rate(double intensity) double E = Au.FieldFromIntensity(intensity, N0); if (E <= 0) return 0; double w = Omega, g = w * Math.Sqrt(Mass * BandGapJ) / (Au.E * E); + if (!(g < 1e6)) return 0; // a field this weak (the far wing of a pulse): G rounds to 1, K(1) is infinite, and the rate is below 1e-100 of any usable one double G = g * g / (1 + g * g), X = 1 / (1 + g * g); var (kG, eG) = IonizationMath.Elliptic(G); var (kX, eX) = IonizationMath.Elliptic(X); @@ -220,7 +222,7 @@ public interface IReportingResponse /// cross section. Several species (air: N2 and O2) ionise independently. In carrier-resolved mode the quasi-static /// (ADK) rate at the instantaneous field is used where the model has one. /// - public sealed class PlasmaResponse : ITimeDomainResponse, IReportingResponse + public sealed class PlasmaResponse : IDispersiveResponse, IReportingResponse { public sealed class Channel { public string Name; public IIonizationRate Rate; public double DensityM3; } public List Channels { get; } = new List(); @@ -230,10 +232,15 @@ public sealed class Channel { public string Name; public IIonizationRate Rate; p public bool Avalanche { get; set; } = true; public bool Absorption { get; set; } = true; public double N0 { get; set; } = 1.0; + /// Free-electron density present before the pulse (a prepared plasma, a discharge), 1/m^3. + public double InitialDensityM3 { get; set; } + /// True (default): the Drude index is applied at every frequency of the pulse, Δn(ω) ∝ 1/(ω (ω + iν)) (); + /// false: at the carrier frequency only. + public bool FrequencyResolved { get; set; } = true; private double _peakRho; private readonly object _lock = new object(); - public string Description => "plasma: " + string.Join(", ", Channels.Select(c => c.Name + " " + c.Rate.Description + string.Format(CultureInfo.InvariantCulture, " ({0:G3} /m^3)", c.DensityM3))) + + public string Description => "plasma" + (FrequencyResolved ? " (Drude index at every frequency)" : " (Drude index at the carrier)") + (InitialDensityM3 > 0 ? string.Format(CultureInfo.InvariantCulture, ", prepared " + "{0:G3} /m^3", InitialDensityM3) : "") + ": " + string.Join(", ", Channels.Select(c => c.Name + " " + c.Rate.Description + string.Format(CultureInfo.InvariantCulture, " ({0:G3} /m^3)", c.DensityM3))) + string.Format(CultureInfo.InvariantCulture, ", tau_c = {0:G3} fs{1}{2}", CollisionTimeS * 1e15, Avalanche ? ", avalanche" : "", double.IsInfinity(RecombinationTimeS) ? "" : ", tau_rec = " + (RecombinationTimeS * 1e15).ToString("G3", CultureInfo.InvariantCulture) + " fs"); public bool NeedsCarrierResolution => false; public double PeakElectronDensityM3 { get { lock (_lock) return _peakRho; } } @@ -245,7 +252,22 @@ public void Report(IDictionary values) if (nt > 0) values["peakIonizationFraction"] = PeakElectronDensityM3 / nt; } - public void Evaluate(ResponseContext ctx, double[] dnRe, double[] dnIm) + /// Everything at the carrier frequency (the Drude term included). + public void Evaluate(ResponseContext ctx, double[] dnRe, double[] dnIm) => Run(ctx, dnRe, dnIm, null, null); + + /// The Drude term (index change and inverse-bremsstrahlung absorption) goes to the dispersive arrays, the energy taken by + /// ionisation and avalanche (a loss at the carrier, not a Drude term) stays in dnIm. + public void EvaluateSplit(ResponseContext ctx, double[] dnRe, double[] dnIm, double[] dispRe, double[] dispIm) => Run(ctx, dnRe, dnIm, dispRe, dispIm); + + /// R(ω) = ω_ref (ω_ref + iν) / (ω (ω + iν)), ν = 1/τ_c: the Drude index Δn = -ω_p² / (2 n0 ω (ω + iν)) relative to its value at ω_ref (Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007), eq. 6 and 15). + public Complex Spectral(double omegaAbs, double omegaRef) + { + if (!(omegaAbs > 0)) return Complex.Zero; + double nu = 1 / CollisionTimeS; + return omegaRef * new Complex(omegaRef, nu) / (omegaAbs * new Complex(omegaAbs, nu)); + } + + private void Run(ResponseContext ctx, double[] dnRe, double[] dnIm, double[] dispRe, double[] dispIm) { int n = ctx.N; double w0 = ctx.Omega0, c = PhysicalConstants.SpeedOfLightMPerS; double n0 = double.IsNaN(ctx.N0) ? N0 : ctx.N0; @@ -255,7 +277,7 @@ public void Evaluate(ResponseContext ctx, double[] dnRe, double[] dnIm) double sigma = k0 * w0 * CollisionTimeS / (n0 * rhoC * (1 + wt * wt)); int K = Channels.Count; var ionised = new double[K]; - double rho = 0, dt = n > 1 ? ctx.T[1] - ctx.T[0] : 0, peak = 0; + double rho = InitialDensityM3, dt = n > 1 ? ctx.T[1] - ctx.T[0] : 0, peak = rho; double uMean = Channels.Count > 0 ? Channels.Average(ch => ch.Rate.EnergyJ) : 1; for (int i = 0; i < n; i++) { @@ -273,11 +295,14 @@ public void Evaluate(ResponseContext ctx, double[] dnRe, double[] dnIm) } double aval = Avalanche ? sigma * I / uMean * rho : 0; // index change at this instant (the plasma made before) - dnRe[i] += -drude * rho; + var dRe = dispRe ?? dnRe; var dIm = dispIm ?? dnIm; + dRe[i] += -drude * rho; if (Absorption) { - dnIm[i] += drude * rho / wt; - if (I > 0) dnIm[i] += (loss + aval * uMean) / I / (2 * k0); + dIm[i] += drude * rho / wt; + // The avalanche takes its energy from the inverse-bremsstrahlung absorption already booked in the Drude + // imaginary part: aval * uMean / (I 2 k0) = sigma rho / (2 k0) = drude rho / wt. Adding it again would count it twice. + if (I > 0) dnIm[i] += loss / I / (2 * k0); } double drho = gen + aval - (double.IsInfinity(RecombinationTimeS) ? 0 : rho / RecombinationTimeS) - RecombinationBetaM3PerS * rho * rho; rho = Math.Max(0, rho + drho * dt); @@ -293,7 +318,8 @@ public static PlasmaResponse For(Material m, double omega0, Serialization.JsonOb spec = spec ?? new Serialization.JsonObject(); string model = spec.GetString("model", m.IsGas ? "ppt" : "keldysh").ToLowerInvariant(); double n0 = m.IndexAt(omega0); - var r = new PlasmaResponse { N0 = n0, Avalanche = spec.GetBool("avalanche", !m.IsGas || spec.Has("collisionTimeS")), Absorption = spec.GetBool("absorption", true) }; + var r = new PlasmaResponse { N0 = n0, Avalanche = spec.GetBool("avalanche", !m.IsGas || spec.Has("collisionTimeS")), Absorption = spec.GetBool("absorption", true), + InitialDensityM3 = spec.GetDouble("electronDensityM3", 0), FrequencyResolved = spec.GetBool("frequencyResolved", true) }; note = null; if (m.IsGas) { diff --git a/src/UltrafastEngine.Core/Physics/Nonlinear.cs b/src/UltrafastEngine.Core/Physics/Nonlinear.cs index b3b3940..62e8c49 100644 --- a/src/UltrafastEngine.Core/Physics/Nonlinear.cs +++ b/src/UltrafastEngine.Core/Physics/Nonlinear.cs @@ -452,7 +452,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) for (int i = 0; i < n; i++) { dnx[i] += cx[i]; dny[i] += cy[i]; } } } - double[] uRe = null, uIm = null; + double[] uRe = null, uIm = null; List disp = null; if (_m.ExtraResponses.Count > 0) { uRe = new double[n]; uIm = new double[n]; @@ -463,13 +463,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) T = _g.Time, TArrival = tArr, TLab = _tLab, Z = z, ElementLength = LengthM, Intensity = it, IntensityX = ix, IntensityY = iy, InstantIntensity = it, Field = null, Omega0 = _g.OmegaCenter, CarrierResolved = false, EffectiveAreaM2 = aeff, ElementId = _id }; - var re = new double[n]; var im = new double[n]; - foreach (var resp in _m.ExtraResponses) - { - Array.Clear(re, 0, n); Array.Clear(im, 0, n); - resp.Evaluate(ctx, re, im); - for (int i = 0; i < n; i++) { uRe[i] += re[i]; uIm[i] += im[i]; } - } + disp = DispersiveResponses.Evaluate(ctx, _m.ExtraResponses, uRe, uIm, false); } var nx = new Complex[n]; @@ -495,7 +489,13 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) if (cubic) AddCubic(ax[i], ay[i], aeff, (1 - fR), ref nx[i], ref ny[i]); } } - double peak = PeakIndexChange(ax, _scalar ? null : ay, nx, ny, _k0); + Complex[] nxP = nx, nyP = ny; + if (disp != null) + { + nxP = (Complex[])nx.Clone(); DispersiveResponses.AddToTime(disp, ax, _k0, nxP); + if (!_scalar) { nyP = (Complex[])ny.Clone(); DispersiveResponses.AddToTime(disp, ay, _k0, nyP); } + } + double peak = PeakIndexChange(ax, _scalar ? null : ay, nxP, nyP, _k0); var nxF = SpectralTransform.TimeToFreq(nx); var nyF = _scalar ? null : SpectralTransform.TimeToFreq(ny); @@ -508,6 +508,11 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) if (nyF != null) nyF[i] *= f; } } + if (disp != null) + { + DispersiveResponses.AddRate(disp, ax, _g.OmegaAbsolute, nxF, 0, 0, double.PositiveInfinity); + if (!_scalar) DispersiveResponses.AddRate(disp, ay, _g.OmegaAbsolute, nyF, 0, 0, double.PositiveInfinity); + } Array.Copy(nxF, 0, rate, 0, n); if (!_scalar) Array.Copy(nyF, 0, rate, n, n); return _k0 * peak; @@ -553,7 +558,7 @@ public FieldGridMapping(FrequencyGrid env, double highestHarmonic = 3.0, int min Field = FrequencyGrid.Create(env.N * p, env.DTime / p, 0.0); CarrierBin = (int)Math.Round(env.OmegaCenter / env.DOmega); if (Math.Abs(CarrierBin * env.DOmega - env.OmegaCenter) > 1e-6 * env.DOmega) - throw new InvalidOperationException("Carrier-resolved mode needs the carrier on a multiple of the frequency step (grid.snapCarrier = true)."); + throw new InvalidOperationException("Carrier-resolved mode needs the carrier on a multiple of the frequency step (grid.snapCarrier = true; a cw source needs it written out)."); int dropped = 0; for (int i = 0; i < env.N; i++) if (FieldIndex(i) <= Field.N / 2) dropped++; DroppedBins = dropped; @@ -667,7 +672,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) ienv[i] = (a[i].Real * a[i].Real + a[i].Imaginary * a[i].Imaginary) / (2 * aeff); iinst[i] = e[i] * e[i] / aeff; } - var dnRe = new double[nf]; var dnIm = new double[nf]; + var dnRe = new double[nf]; var dnIm = new double[nf]; List disp = null; double fR = _m.EffectiveRamanFraction; if (!_m.ReplaceKerr && _m.N2 != 0) { @@ -684,13 +689,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) T = g.Time, TArrival = tArr, TLab = _tLab, Z = z, ElementLength = LengthM, Intensity = ienv, IntensityX = ienv, IntensityY = new double[nf], InstantIntensity = iinst, Field = e, Omega0 = _map.Envelope.OmegaCenter, CarrierResolved = true, EffectiveAreaM2 = aeff, ElementId = _id, N0 = N0 }; - var re = new double[nf]; var im = new double[nf]; - foreach (var r in _m.ExtraResponses) - { - Array.Clear(re, 0, nf); Array.Clear(im, 0, nf); - r.Evaluate(ctx, re, im); - for (int i = 0; i < nf; i++) { dnRe[i] += re[i]; dnIm[i] += im[i]; } - } + disp = DispersiveResponses.Evaluate(ctx, _m.ExtraResponses, dnRe, dnIm, false); } double peak = 0; var gr = new Complex[nf]; var gi = new Complex[nf]; @@ -703,6 +702,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) double pk = Math.Abs(dnRe[i]); if (pk > peak) peak = pk; } + if (disp != null) foreach (var t in disp) for (int i = 0; i < nf; i++) { double pk = Math.Abs(t.Re[i]); if (pk > peak) peak = pk; } var GR = SpectralTransform.TimeToFreq(gr); var GI = anyIm ? SpectralTransform.TimeToFreq(gi) : null; double c = PhysicalConstants.SpeedOfLightMPerS, w0 = _map.Envelope.ReportOmega; @@ -720,6 +720,11 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) rate[j] = v; } rate[nf / 2] = Complex.Zero; + if (disp != null) + { + var ec = new Complex[nf]; for (int i = 0; i < nf; i++) ec[i] = e[i]; + DispersiveResponses.AddRate(disp, ec, g.OmegaAbsolute, rate, 0, nf / 2 + 1, wCut); + } return peak * w0 / c * 0.75; // cycle-average of (2/3)*2*I -> I : report envelope-equivalent peak } } diff --git a/src/UltrafastEngine.Core/Results/RunResult.cs b/src/UltrafastEngine.Core/Results/RunResult.cs index f967f44..6ee3485 100644 --- a/src/UltrafastEngine.Core/Results/RunResult.cs +++ b/src/UltrafastEngine.Core/Results/RunResult.cs @@ -152,6 +152,7 @@ public JsonObject SummaryJson() o.Set("arrays", new JsonArray(Arrays().Select(a => (JsonValue)new JsonObject().Set("name", a.Name).Set("shape", JsonValue.FromArray(a.Shape.Select(x => (double)x))).Set("unit", a.Unit).Set("description", a.Description)))); o.Set("notes", JsonValue.FromArray(Notes)); o.Set("warnings", JsonValue.FromArray(Warnings)); + o.Set("unverifiedInputs", Compiled?.Provenance?.UnverifiedInputsJson() ?? new JsonArray()); return o; } diff --git a/src/UltrafastEngine.Core/Schema/BuiltInElements.cs b/src/UltrafastEngine.Core/Schema/BuiltInElements.cs index d888b87..488be00 100644 --- a/src/UltrafastEngine.Core/Schema/BuiltInElements.cs +++ b/src/UltrafastEngine.Core/Schema/BuiltInElements.cs @@ -303,13 +303,14 @@ private static void Elements(Action add) .Obj("inversion", "fraction or smallSignalGain (instead of / before a pump)", "inversion") .Model("beam", "beam", "Signal beam").Num("modeRadiusM", "m", "Signal 1/e^2 radius (mode area pi w^2 / 2)") .Obj("transverse", "Spatial gain", "transverse") - .P("thermalLens", ParamType.Any, "", "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam; passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence (docs/reference/gain.md#thermal-lens)") + .P("thermalLens", ParamType.Any, "", "true / false or {rodRadiusM, heatFraction, apply, track, focalLengthM, passGapM, stress}: track (default true; amplifier: only when given) puts the thermal lens on the tracked Gaussian beam as a graded-index duct (a regenerative amplifier also needs an integer roundTrips); passGapM is the free path between amplifier passes; focalLengthM is a measured thermal focal length used instead of the heat model; stress adds thermal birefringence: a lumped depolarisation, or the four-lobed pattern on a cartesian transverse grid (docs/reference/gain.md#thermal-lens)") .Int("slices", "Slices along z", "24").Int("segments", "Time-ordered saturation groups", "auto") .Bool("spm", "SPM of the host", "true").Str("host", "Host material (dispersion, n2)") .Num("repRateHz", "Hz", "Repetition rate (overrides the pulse's)") .P("steadyState", ParamType.Any, "", "true / false or {maxPulses, tolerance}", "true") .Model("dopingProfile", "field", "Position-dependent doping factor") .Bool("ase", "Seed amplified spontaneous emission (1-D)", "false").Int("aseSeed", "ASE random seed") + .P("aseModel", ParamType.Any, "", "true / false or {solidAngleSr, polarizations, points, bandLowerM, bandUpperM}: inter-pulse ASE in both directions over the inversion lifetime; it depletes the inversion between pulses and reports the ASE power in the signal mode (docs/reference/gain.md#ase)", "false") .Num("temperatureK", "K", "Crystal temperature").Num("dopingDensityM3", "1/m^3", "Doping density (overrides the medium's)")); add(new ComponentSpec("pump", "", "A pump for gain media.") .Enum("kind", new[] { "cw", "pulsed", "none" }, "Pump type", "cw") diff --git a/src/UltrafastEngine.Core/Schema/BuiltInOptions.cs b/src/UltrafastEngine.Core/Schema/BuiltInOptions.cs index 020511d..9c00a02 100644 --- a/src/UltrafastEngine.Core/Schema/BuiltInOptions.cs +++ b/src/UltrafastEngine.Core/Schema/BuiltInOptions.cs @@ -107,7 +107,7 @@ private static void Sections(Action add) .Int("n", "Number of samples (with auto: a minimum)", "4096") .Num("windowS", "s", "Time window", "30 x the transform limit").Num("windowPs", "ps", "Time window in ps") .Int("maxN", "Largest allowed n", "4194304") - .Bool("snapCarrier", "Put the carrier on the frequency grid", "true") + .Bool("snapCarrier", "Put the carrier on the frequency grid. A cw source is not snapped (its line sits on centerWavelengthM) unless this is written", "true") .Num("spectralMargin", "", "Spectral span factor", "auto") .Num("minSpanFraction", "", "Nonlinear runs: minimum half-span / carrier", "0.1") .Num("minPointsPerFwhm", "", "Samples across the transform-limited FWHM", "12") @@ -122,6 +122,8 @@ private static void Sections(Action add) .Enum("fieldMode", new[] { "auto", "envelope", "carrier" }, "Envelope or carrier-resolved (real field) propagation", "auto") .Num("fieldHarmonic", "", "Highest harmonic the carrier-resolved grid holds", "3") .Bool("absoluteIndex", "Add air dispersion to air paths quoted relative to air", "true") + .P("strictMaterialsAllow", ParamType.Array, "", "Catalogue quantities that strictMaterials accepts although they are not verified, by catalogue path (e.g. \"CS2.n2\") as listed in the error and in summary.json unverifiedInputs; matching is case-insensitive and an entry name such as \"CS2\" accepts all its quantities") + .Bool("strictMaterials", "Refuse a run that uses approximate or unverified material-catalogue values (the compile error lists them; override each, or give a verified value in your own catalogue file). The environment variable ULTRAFAST_STRICT_MATERIALS=1 does the same", "false") .Enum("polarization", new[] { "jones", "scalar" }, "Jones (two polarisations) or scalar", "jones") .Bool("losses", "false removes flat loss terms", "true") .P("beamTracking", ParamType.Any, "", "Gaussian beam (q) tracking: \"auto\" (on with a Gaussian inputBeam), \"on\", \"off\"", "\"auto\"") @@ -134,7 +136,7 @@ private static void Sections(Action add) .Num("bandGapEv", "eV", "Band gap (solids)").Num("effectiveMass", "", "Reduced mass / m_e (solids)").Num("neutralDensityM3", "1/m^3", "Density of ionisable atoms / valence electrons") .Num("sigmaK", "(m^2/W)^K/s", "MPI cross section").Int("photons", "MPI order K") .Num("collisionTimeS", "s", "Electron collision time").Num("recombinationTimeS", "s", "Recombination / trapping time").Num("recombinationBetaM3PerS", "m^3/s", "Two-body recombination") - .Bool("avalanche", "Avalanche (impact) ionisation", "true for solids").Bool("absorption", "Plasma and ionisation losses", "true").Str("species", "Gas species (when the material does not say)")); + .Num("electronDensityM3", "1/m^3", "Free-electron density present before the pulse (a prepared plasma)", "0").Bool("frequencyResolved", "Apply the Drude index at every frequency of the pulse (1/(omega (omega + i nu))); false: at the carrier only", "true").Bool("avalanche", "Avalanche (impact) ionisation", "true for solids").Bool("absorption", "Plasma and ionisation losses", "true").Str("species", "Gas species (when the material does not say)")); add(new ComponentSpec("coherence", "", "A Gaussian-Schell beam as an incoherent sum of Hermite-Gauss modes (docs/reference/transverse.md#partial-coherence).") .Enum("model", new[] { "gaussianSchell" }, "Coherence model", "gaussianSchell") .Num("m2", "", "Beam quality M^2 >= 1", required: true) diff --git a/src/UltrafastEngine.Core/Specs/RunSpec.cs b/src/UltrafastEngine.Core/Specs/RunSpec.cs index bfa3e0f..c6fd1fc 100644 --- a/src/UltrafastEngine.Core/Specs/RunSpec.cs +++ b/src/UltrafastEngine.Core/Specs/RunSpec.cs @@ -21,6 +21,8 @@ public sealed class GridSpec /// Put the carrier on an exact multiple of the frequency step (needed by carrier-resolved mode; /// shifts the carrier by < half a bin). public bool SnapCarrier { get; set; } = true; + /// snapCarrier was written in the spec. A cw source puts its single line on the requested carrier (no snapping) unless it was. + public bool SnapCarrierGiven { get; set; } /// Spectral span factor applied to the input spectrum (-40 dB width) when sizing the time step. /// NaN: 1.3 for linear systems, 3 when any element is nonlinear. public double SpectralMargin { get; set; } = double.NaN; @@ -61,6 +63,7 @@ public static GridSpec FromJson(JsonObject o) g.Auto = o.GetBool("auto", true); g.MaxN = o.GetInt("maxN", g.MaxN); g.SnapCarrier = o.GetBool("snapCarrier", true); + g.SnapCarrierGiven = o.Has("snapCarrier"); g.SpectralMargin = o.GetDouble("spectralMargin", double.NaN); g.MinSpanFraction = o.GetDouble("minSpanFraction", 0.1); g.MinPointsPerFwhm = o.GetDouble("minPointsPerFwhm", 12); @@ -101,6 +104,10 @@ public sealed class PhysicsOptions public double FieldHarmonic { get; set; } = 3.0; /// Add the dispersion of air for air paths quoted relative to air (Zemax convention). public bool AbsoluteIndex { get; set; } = true; + /// Refuse a run that uses approximate or unverified material-catalogue values (also: ULTRAFAST_STRICT_MATERIALS=1). + public bool StrictMaterials { get; set; } + /// Catalogue paths (or path prefixes) that strict mode accepts although they are not verified. + public List StrictMaterialsAllow { get; } = new List(); /// "jones" (full 2x2 polarisation) or "scalar" (s/p averaged). public string Polarization { get; set; } = "jones"; public bool Losses { get; set; } = true; @@ -126,6 +133,8 @@ public static PhysicsOptions FromJson(JsonObject o) p.FieldMode = o.GetString("fieldMode", "auto"); p.FieldHarmonic = o.GetDouble("fieldHarmonic", 3.0); p.AbsoluteIndex = o.GetBool("absoluteIndex", true); + p.StrictMaterials = o.GetBool("strictMaterials", false); + var allow = o.GetArray("strictMaterialsAllow"); if (allow != null) foreach (var a in allow) p.StrictMaterialsAllow.Add(a.AsString()); p.Polarization = o.GetString("polarization", "jones"); p.Losses = o.GetBool("losses", true); p.Transverse = o.Has("transverse") ? o["transverse"] : null; diff --git a/src/UltrafastEngine.Core/Systems/SystemGraph.cs b/src/UltrafastEngine.Core/Systems/SystemGraph.cs index d39c954..2f40885 100644 --- a/src/UltrafastEngine.Core/Systems/SystemGraph.cs +++ b/src/UltrafastEngine.Core/Systems/SystemGraph.cs @@ -14,7 +14,7 @@ // static class Ports // string[] Inputs(SystemElement e), Outputs(SystemElement e) declared in Schema/BuiltIn*.cs (default: in, out) // -// Design: docs/design/branching.md, sections 2 and 3. Reference: docs/reference/branches.md. +// Reference: docs/reference/branches.md. using System; using System.Collections.Generic; diff --git a/src/UltrafastEngine.Core/Transverse/BeamShapes.cs b/src/UltrafastEngine.Core/Transverse/BeamShapes.cs index 01b4fe8..41c7eba 100644 --- a/src/UltrafastEngine.Core/Transverse/BeamShapes.cs +++ b/src/UltrafastEngine.Core/Transverse/BeamShapes.cs @@ -162,7 +162,7 @@ private static bool Locate(double[] a, double v, out int i, out double t) return true; } - /// Bilinear sample (linear on a radial map) of a map array at (x, y); 0 outside the map. + /// Bilinear sample (linear on a radial map) of a map array at (x, y); 0 outside the map. An axis of one sample is constant. public double Sample(double[] d, double x, double y) { if (d == null) return 0; @@ -171,7 +171,10 @@ public double Sample(double[] d, double x, double y) if (!Locate(R, Math.Sqrt(x * x + y * y), out int i, out double t)) return 0; return d[i] * (1 - t) + d[Math.Min(i + 1, R.Length - 1)] * t; } - if (!Locate(X, x, out int ix, out double u) || !Locate(Y, y, out int iy, out double v)) return 0; + // an axis with one sample carries no variation along it (a map of one row, e.g. the row of a "1-D" scene saved at y = -dx/2, which a grid with ny = 1 never samples + // exactly): it is constant along that axis instead of zero everywhere but at its single coordinate + int ix = 0, iy = 0; double u = 0, v = 0; + if ((X.Length > 1 && !Locate(X, x, out ix, out u)) || (Y.Length > 1 && !Locate(Y, y, out iy, out v))) return 0; int ny = Y.Length, ix1 = Math.Min(ix + 1, X.Length - 1), iy1 = Math.Min(iy + 1, ny - 1); return d[ix * ny + iy] * (1 - u) * (1 - v) + d[ix1 * ny + iy] * u * (1 - v) + d[ix * ny + iy1] * (1 - u) * v + d[ix1 * ny + iy1] * u * v; } diff --git a/src/UltrafastEngine.Core/Transverse/SpatialPropagation.cs b/src/UltrafastEngine.Core/Transverse/SpatialPropagation.cs index c2b5af4..20c4cf2 100644 --- a/src/UltrafastEngine.Core/Transverse/SpatialPropagation.cs +++ b/src/UltrafastEngine.Core/Transverse/SpatialPropagation.cs @@ -260,6 +260,7 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) for (int i = 0; i < n; i++) { dnX[i] += sc * cx[i]; dnY[i] += sc * cy[i]; dnXY[i] = sc * cxy[i]; } } } + List disp = null; if (_m.ExtraResponses.Count > 0) { var tl = new double[n]; for (int i = 0; i < n; i++) tl[i] = _g.Time[i] + tArr; @@ -269,13 +270,9 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) InstantIntensity = I, Field = null, Omega0 = _g.OmegaCenter, CarrierResolved = false, EffectiveAreaM2 = _b.Weights[m], ElementId = _id, X = _b.X[m], Y = _b.Y[m], Spatial = true }; - var re = new double[n]; var im = new double[n]; - foreach (var resp in _m.ExtraResponses) - { - Array.Clear(re, 0, n); Array.Clear(im, 0, n); - lock (resp) resp.Evaluate(ctx, re, im); - for (int i = 0; i < n; i++) { dnX[i] += re[i]; if (Vector) dnY[i] += re[i]; dnI[i] += im[i]; } - } + var ure = new double[n]; var uim = new double[n]; + disp = Physics.DispersiveResponses.Evaluate(ctx, _m.ExtraResponses, ure, uim, true); + for (int i = 0; i < n; i++) { dnX[i] += ure[i]; if (Vector) dnY[i] += ure[i]; dnI[i] += uim[i]; } } var nt = new Complex[n]; var nty = Vector ? new Complex[n] : null; double cc = kerr && Vector ? sc * _m.N2 * (1 - fR) / 3.0 : 0; @@ -294,11 +291,22 @@ public double Nonlinear(Complex[] state, double z, Complex[] rate) if (ramXY != null) { double d = sc * _m.N2 * fR * ramXY[i]; nt[i] += Complex.ImaginaryOne * _k0 * d * ey[i]; nty[i] += Complex.ImaginaryOne * _k0 * d * e[i]; } } } - double pk = Physics.EnvelopeProblem.PeakIndexChange(e, ey, nt, nty, _k0); + Complex[] ntP = nt, ntyP = nty; + if (disp != null) + { + ntP = (Complex[])nt.Clone(); Physics.DispersiveResponses.AddToTime(disp, e, _k0, ntP); + if (Vector) { ntyP = (Complex[])nty.Clone(); Physics.DispersiveResponses.AddToTime(disp, ey, _k0, ntyP); } + } + double pk = Physics.EnvelopeProblem.PeakIndexChange(e, ey, ntP, ntyP, _k0); var nf = nt; SpectralTransform.TimeToFreqInPlace(nf); Complex[] nfy = null; if (Vector) { nfy = nty; SpectralTransform.TimeToFreqInPlace(nfy); } if (_m.EnableSelfSteepening) for (int i = 0; i < n; i++) { double f = _g.OmegaAbsolute[i] / _g.ReportOmega; nf[i] *= f; if (Vector) nfy[i] *= f; } + if (disp != null) + { + Physics.DispersiveResponses.AddRate(disp, e, _g.OmegaAbsolute, nf, 0, 0, double.PositiveInfinity); + if (Vector) Physics.DispersiveResponses.AddRate(disp, ey, _g.OmegaAbsolute, nfy, 0, 0, double.PositiveInfinity); + } if (_props != null) { for (int i = 0; i < n; i++) nf[i] += StaticRate(ef[i], i, m, dnS, alS); @@ -350,6 +358,13 @@ public static TransverseProfile Initial(TransverseSpec spec, ITransverseBasis ba var spectrum = p.SpectralIntensity(); var bins = TransverseOps.SampleBins(spectrum, spec.SpectralSamples); var omegas = bins.Select(b => p.Grid.OmegaAbsolute[b]).ToArray(); + if (bins.Length == 1) + { + // one stored frequency: the energy-centroid frequency itself, not the lattice bin next to it (up to half a bin = 4e-7 of the carrier for a 1 ns + // pulse away), so that a pulse and a cw line at the same carrier see the same device; kept on the bin when the centroid falls in a hole of the spectrum + double s0 = 0, s1 = 0; for (int i = 0; i < spectrum.Length; i++) { s0 += spectrum[i]; s1 += spectrum[i] * p.Grid.OmegaAbsolute[i]; } + if (s0 > 0 && spectrum[bins[0]] >= 1e-3 * spectrum.Max() && Math.Abs(s1 / s0 - omegas[0]) <= p.Grid.DOmega) omegas[0] = s1 / s0; + } double wx = double.NaN, wy = double.NaN, R = double.PositiveInfinity; if (beam != null) (wx, wy, R) = TransverseOps.BeamAt(beam, 0); string mode = spec.Transport; diff --git a/src/UltrafastEngine.Core/UltrafastEngine.Core.csproj b/src/UltrafastEngine.Core/UltrafastEngine.Core.csproj index 07fe6c9..0b4803a 100644 --- a/src/UltrafastEngine.Core/UltrafastEngine.Core.csproj +++ b/src/UltrafastEngine.Core/UltrafastEngine.Core.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/UltrafastEngine.Native/Exports.cs b/src/UltrafastEngine.Native/Exports.cs index ae65412..b3ac8b8 100644 --- a/src/UltrafastEngine.Native/Exports.cs +++ b/src/UltrafastEngine.Native/Exports.cs @@ -76,6 +76,9 @@ internal static void Init() #endif #if UF_WITH_HITRAN UltrafastEngine.Plugins.Hitran.HitranPlugin.RegisterStatic(); +#endif +#if UF_WITH_AO + UltrafastEngine.Plugins.Ao.AoPlugin.RegisterStatic(); #endif } } diff --git a/src/UltrafastEngine.Native/UltrafastEngine.Native.csproj b/src/UltrafastEngine.Native/UltrafastEngine.Native.csproj index c388526..b609076 100644 --- a/src/UltrafastEngine.Native/UltrafastEngine.Native.csproj +++ b/src/UltrafastEngine.Native/UltrafastEngine.Native.csproj @@ -47,6 +47,13 @@ + + + $(DefineConstants);UF_WITH_AO + + + + diff --git a/tests/UltrafastEngine.Tests/AoDeviceTests.cs b/tests/UltrafastEngine.Tests/AoDeviceTests.cs index 5b1dca7..1473af5 100644 --- a/tests/UltrafastEngine.Tests/AoDeviceTests.cs +++ b/tests/UltrafastEngine.Tests/AoDeviceTests.cs @@ -185,7 +185,7 @@ public void Slm_PhaseTarget_IsMappedToTheNearestGreyLevel_OfANonlinearCurve() for (int i = 0; i < fa.Length; i++) Assert.Close(fb[i], fa[i], 1e-12, 1e-30, "fluence " + i); } - [Test(Slow = true, Reference = "S2 scale spike (docs/design/notes/spikes.md): a 1920 x 1080 panel on a 2048 x 1280 grid, one frequency, ran in 1.9 s; here the panel is the slm element written with 8-bit grey levels (a phase-only lens-and-grating pattern from a callback); a Gaussian overfilling the panel passes the Gaussian power on the panel", Tolerance = "budget 13 s (the S2 limit; 45 s when the CI variable is set, shared runners took 21-23 s); transmission 1e-3 of the erf product")] + [Test(Slow = true, Reference = "S2 scale spike (docs/design/history.md): a 1920 x 1080 panel on a 2048 x 1280 grid, one frequency, ran in 1.9 s; here the panel is the slm element written with 8-bit grey levels (a phase-only lens-and-grating pattern from a callback); a Gaussian overfilling the panel passes the Gaussian power on the panel", Tolerance = "budget 13 s (the S2 limit; 45 s when the CI variable is set, shared runners took 21-23 s); transmission 1e-3 of the erf product")] public void Slm_FullPanel_1920x1080_8Bit_RunsWithinTheScaleBudget() { int nx = 1920, ny = 1080; double pitch = 8e-6, w = 6e-3; @@ -311,5 +311,74 @@ public void SpectralShaper_LinearPixelPhase_DelaysThePulse() Assert.Close(tau, mt / m, 0, 2e-15, "delay"); Assert.Close(1, r0.Output.TotalEnergyJ() / r1.Output.TotalEnergyJ(), 0.01, 0, "lossless apart from the spot blur"); } + + // ------------------------------------------------------------------------------------------ grid-planner hook for shapers + private static RunResult AutoGrid(JsonObject el, double fwhm, JsonObject grid) => Go(Sys(el), Spec(Gauss(800e-9, fwhm, 1e-6), O("grid", grid))); + + [Test(Reference = "Weiner (2000): side pulses of a pixelated mask lie at 2 pi / pixel bandwidth; the planner must hold them without grid.windowS (window >= 2 x 1.25 x T, T = 1.6 ps here)", Tolerance = "window at least 2 x 1.25 x T; no warning; the +T side pulse is in the output")] + public void Planner_SpectralShaper_WidensTheWindow_ForItsSidePulses() + { + double lam = 800e-9, c = 299792458.0, T = 1.6e-12, dw = 2 * Math.PI / T, D = 1e5, f = 0.75; + double pitch = D * lam * lam * dw / (2 * Math.PI * c), w0 = pitch / 40; + var r = AutoGrid(O("id", "s", "type", "spectralShaper", "pixels", 161, "pixelPitchM", pitch, "dispersionMPerM", D, "spotRadiusM", w0, "fillFactor", f, "gapTransmission", 0.0), 150e-15, O()); + double window = r.Output.Grid.Time.Length * r.Output.Grid.DTime; + Assert.True(window >= 2 * 1.25 * T, "window " + window + " holds the side pulses at +-T"); + Assert.Equal(0, r.Warnings.Count, "no wrap warning: " + string.Join(" | ", r.Warnings) + " NOTES " + string.Join(" | ", r.Notes.Where(x => x.Contains("ndow") || x.Contains("atellite")))); + var t = TimeAxis(r); var p = Power(r); double plus = 0, main = 0; + for (int i = 0; i < t.Length; i++) { if (Math.Abs(t[i]) < 0.5e-12) main += p[i]; else if (Math.Abs(t[i] - T) < 0.5e-12) plus += p[i]; } + Assert.True(plus / main > 0.05, "the +T side pulse is in the window (ratio " + plus / main + ")"); + } + + [Test(Reference = "a flat mask makes no side pulses: the planner adds no room (same grid as an aopdf without programmed phase)", Tolerance = "identical grid")] + public void Planner_SpectralShaper_FlatMask_AddsNoRoom() + { + double pitch = 2e-4; int n = 121; + var a = AutoGrid(O("id", "s", "type", "spectralShaper", "pixels", n, "pixelPitchM", pitch, "dispersionMPerM", 1e5, "spotRadiusM", pitch / 10), 30e-15, O()); + var b = AutoGrid(O("id", "s", "type", "aopdf"), 30e-15, O()); + Assert.Equal(b.Output.Grid.Time.Length, a.Output.Grid.Time.Length, "grid size"); + Assert.Close(b.Output.Grid.DTime, a.Output.Grid.DTime, 1e-12, 0, "dt"); + } + + [Test(Reference = "GDD x bandwidth: a transform-limited Gaussian of FWHM tau and GDD phi'' has FWHM tau sqrt(1 + (4 ln2 phi''/tau^2)^2) (Diels and Rudolph, Ultrashort Laser Pulse Phenomena, ch. 1); the auto-sized window must hold it", Tolerance = "window >= 2 x the stretched FWHM, stretched FWHM within 2 % of the formula")] + public void Planner_Aopdf_Gdd_WidensTheWindow() + { + double tau = 30e-15, gdd = 4e4 * 1e-30; + double expect = tau * Math.Sqrt(1 + Math.Pow(4 * Math.Log(2) * gdd / (tau * tau), 2)); + var r = AutoGrid(O("id", "d", "type", "aopdf", "gddFs2", 4e4), tau, O()); + Assert.True(r.Output.Grid.Time.Length * r.Output.Grid.DTime >= 2 * expect, "window holds the stretched pulse"); + Assert.Close(expect, PulseMetrics.Fwhm(TimeAxis(r), Power(r)), 0.02, 0, "stretched FWHM"); + } + + [Test(Reference = "fixed window (grid.auto false): a programmed GDD x bandwidth stretch of about 1 ns on a 10 ps window wraps, so the planner warns", Tolerance = "a warning that says the grid is too small")] + public void Planner_Aopdf_FixedWindow_WarnsAboutWrap() + { + double tau = 30e-15, gddFs2 = 1e-9 / (2 * Math.PI * 0.441 / tau) * 1e30; // GDD x (0.441 x 2 pi / tau) = 1 ns + var r = AutoGrid(O("id", "d", "type", "aopdf", "gddFs2", gddFs2), tau, O("auto", false, "windowS", 10e-12)); + Assert.True(r.Warnings.Any(w => w.Contains("Grid looks too small")), "wrap warning: " + string.Join(" | ", r.Warnings)); + } + + [Test(Reference = "Littrow condition (Hecht, Optics): for sin(theta) = m lambda N / 2 a grating in the focal plane of an f lens gives dx/dlambda = 2 f tan(theta) / lambda; the shaper built from the grating data equals the one given that dispersion", Tolerance = "output power identical to 1e-12 of its peak")] + public void SpectralShaper_GratingDispersion_MatchesLittrowReference() + { + double lam = 800e-9, lines = 1200e3, f = 0.25, th = Math.Asin(lam * lines / 2); + double D = 2 * f * Math.Tan(th) / lam, pitch = 5e-5; int n = 161; + var ph = new JsonArray(); for (int j = 0; j < n; j++) ph.Add(0.7 * Math.Sin(0.3 * j)); + var a = Temporal(O("id", "s", "type", "spectralShaper", "pixels", n, "pixelPitchM", pitch, "spotRadiusM", pitch / 8, "phaseRad", ph, "phaseWrapRad", 0.0, "gratingLinesPerMm", lines / 1e3, "focalLengthM", f, "incidenceAngleRad", th), 30e-15, 8e-12); + var b = Temporal(O("id", "s", "type", "spectralShaper", "pixels", n, "pixelPitchM", pitch, "spotRadiusM", pitch / 8, "phaseRad", ph, "phaseWrapRad", 0.0, "dispersionMPerM", D), 30e-15, 8e-12); + var pa = Power(a); var pb = Power(b); double pk = pb.Max(), worst = 0; + for (int i = 0; i < pa.Length; i++) worst = Math.Max(worst, Math.Abs(pa[i] - pb[i]) / pk); + Assert.Less(worst, 1e-12, "power differs from the Littrow dispersion by " + worst); + } + + [Test(Reference = "the unknown-element message names the ao plugin for its four types", Tolerance = "message text")] + public void UnknownElement_NamesTheAoPlugin() + { + foreach (var type in new[] { "deformableMirror", "slm", "spectralShaper", "aopdf" }) + { + string msg = UltrafastEngine.Engine.Compiler.UnknownElementMessage(type, "x"); + Assert.True(msg.Contains("ao plugin") && msg.Contains(type), type + ": " + msg); + } + Assert.True(UltrafastEngine.Engine.Compiler.UnknownElementMessage("nosuch", "x").Contains("Known types"), "other types still list the known ones"); + } } } diff --git a/tests/UltrafastEngine.Tests/AseStressTests.cs b/tests/UltrafastEngine.Tests/AseStressTests.cs new file mode 100644 index 0000000..bd92eb6 --- /dev/null +++ b/tests/UltrafastEngine.Tests/AseStressTests.cs @@ -0,0 +1,233 @@ +using System; +using System.Linq; +using System.Numerics; +using UltrafastEngine.Gain; +using UltrafastEngine.Materials; +using UltrafastEngine.Results; +using UltrafastEngine.Serialization; +using UltrafastEngine.Transverse; +using static UltrafastEngine.Tests.U; + +namespace UltrafastEngine.Tests +{ + /// Session D (WP2): ASE over the inversion lifetime (AseModel) and the four-lobed stress-birefringence map. + [TestArea("Inter-pulse ASE (both directions, spectrally resolved) and the stress-birefringence pattern")] + public class AseStressTests + { + const double H = GainMedium.H, C = GainMedium.C; + private static JsonObject Grid(int n, double window) => O("grid", O("auto", false, "n", n, "windowS", window)); + + /// A medium with flat cross sections (m^2) so the closed forms apply; 1-D crystal of the given mode area. + private static GainCrystal FlatCrystal(double se, double sa, double nt, double lengthM, int nz, double modeRadiusM, out GainMedium m) + { + m = new GainMedium { Name = "flat", LifetimeS = 1e-3, DopingDensityM3 = nt, EmissionBand = _ => se, AbsorptionBand = _ => sa }; + return new GainCrystal(m, lengthM, nz, new PumpSpec(), modeRadiusM, null, null); + } + + private static AseModel Model(GainCrystal gc, double[] nu, double dnu, int pol = 2, double n = 1.82) + => new AseModel(gc, new AseSpec { Enabled = true, Polarizations = pol }, n, 2 * Math.PI * nu[0], nu, dnu); + + [Test(Category = "gain", Reference = "single-mode ASE per polarisation P = n_sp h nu dnu (G - 1), n_sp = sigma_e N2 / (sigma_e N2 - sigma_a N1) (Desurvire, Erbium-doped fiber amplifiers, 1994; Siegman, Lasers, ch. 6)", Tolerance = "1e-12 (uniform inversion: the slice step is exact)")] + public void Ase_UniformInversion_IsNspHnuDnuGMinusOne_BothEnds_BothPolarisations() + { + double se = 2.0e-24, sa = 0.2e-24, nt = 1.4e26, L = 5e-3, w = 0.4e-3, nu = C / 1030e-9, dnu = 1e12, beta = 0.3; + var gc = FlatCrystal(se, sa, nt, L, 24, w, out _); + gc.SetInversionFraction(beta); + double A = gc.CellArea[0], n2 = beta * nt, gam = se * n2 - sa * (nt - n2), G = Math.Exp(gam * L), nsp = se * n2 / gam; + foreach (int pol in new[] { 1, 2 }) + { + var o = Model(gc, new[] { nu }, dnu, pol).ModeOutput(gc.N2, gc.Nt, 0, A); + double expected = pol * nsp * H * nu * dnu * (G - 1); + Assert.Close(expected, o.ForwardW, 1e-12, 0, "forward, " + pol + " polarisation(s)"); + Assert.Close(expected, o.BackwardW, 1e-12, 0, "backward equals forward for a uniform inversion"); + } + } + + [Test(Category = "gain", Reference = "dI/dz = gamma(z) I + S(z) integrated with a fine RK4 for a linear inversion profile", Tolerance = "1e-6 (the model holds N2 constant in each of 400 slices, the reference is continuous: O(1/nz^2))")] + public void Ase_NonUniformInversion_MatchesADirectIntegration_AndForwardDiffersFromBackward() + { + double se = 2.0e-24, sa = 0.2e-24, nt = 1.4e26, L = 5e-3, w = 0.4e-3, nu = C / 1030e-9, dnu = 1e12; + int nz = 400; + var gc = FlatCrystal(se, sa, nt, L, nz, w, out _); + for (int j = 0; j < nz; j++) gc.N2[j] = nt * (0.1 + 0.5 * (j + 0.5) / nz); // beta rises from 0.1 to 0.6 along z + double A = gc.CellArea[0]; + var o = Model(gc, new[] { nu }, dnu).ModeOutput(gc.N2, gc.Nt, 0, A); + double N2At(double z) { double b = 0.1 + 0.5 * z / L; return nt * b; } + double Rhs(double z, double I, double dir) { double n2 = N2At(z); return dir * ((se * n2 - sa * (nt - n2)) * I) + 2 * H * nu * se * n2 / A; } + double Integrate(int dir) + { + int ns = 20000; double dz = L / ns, I = 0; + for (int s = 0; s < ns; s++) + { + double z = dir > 0 ? s * dz : L - s * dz, h = dir * 0 + dz; + // march along the propagation direction: parameter t from the entrance + double zz(double t) => dir > 0 ? z + t : z - t; + double k1 = Rhs(zz(0), I, 1), k2 = Rhs(zz(h / 2), I + h / 2 * k1, 1), k3 = Rhs(zz(h / 2), I + h / 2 * k2, 1), k4 = Rhs(zz(h), I + h * k3, 1); + I += h / 6 * (k1 + 2 * k2 + 2 * k3 + k4); + } + return I; + } + // the gain gamma is the same for both directions; Rhs with dir = 1 marches along each direction's own path + double fwd = Integrate(1) * A * dnu, bwd = Integrate(-1) * A * dnu; + Assert.Close(fwd, o.ForwardW, 1e-6, 0, "forward"); + Assert.Close(bwd, o.BackwardW, 1e-6, 0, "backward"); + Assert.Greater(Math.Abs(o.ForwardW - o.BackwardW) / o.ForwardW, 0.01, "the two ends differ when the inversion is not uniform"); + } + + [Test(Category = "gain", Reference = "Fuchtbauer-Ladenburg: 1 / tau_rad = 8 pi n^2 integral sigma_e(nu) nu^2 / c^2 dnu (Aull and Jenssen, IEEE JQE 18, 925 (1982)); the source into 4 pi, both polarisations, is N2 / tau_rad", Tolerance = "1e-9 (quadrature of the same integrals)")] + public void Ase_SpontaneousSource_IntoFourPi_IsTheRadiativeDecay() + { + // a Gaussian emission band, 2 nm wide at 1030 nm + double nu0 = C / 1030e-9, sigNu = 0.4e12, peak = 2e-24, n = 1.82, nt = 1e26; + var m = new GainMedium { Name = "g", LifetimeS = 1e-3, DopingDensityM3 = nt, EmissionBand = o => peak * Math.Exp(-0.5 * Math.Pow((o / (2 * Math.PI) - nu0) / sigNu, 2)), AbsorptionBand = _ => 0 }; + var gc = new GainCrystal(m, 1e-3, 4, new PumpSpec(), 1e-4, null, null); + int K = 4000; double lo = nu0 - 8 * sigNu, dnu = 16 * sigNu / K; + var nus = Enumerable.Range(0, K).Select(i => lo + (i + 0.5) * dnu).ToArray(); + var ase = new AseModel(gc, new AseSpec { Enabled = true, Polarizations = 2 }, n, 2 * Math.PI * nu0, nus, dnu); + double n2 = 0.1 * nt, power = 0, invTau = 0, hnuWeighted = 0; + for (int i = 0; i < K; i++) + { + double se = ase.SigmaE[i]; + power += 2 * H * nus[i] * se * n2 * ase.Etendue(nus[i], 4 * Math.PI) * dnu; // the source term of the model, W / m^3 + double w = 8 * Math.PI * n * n * se * nus[i] * nus[i] / (C * C) * dnu; + invTau += w; hnuWeighted += w * H * nus[i]; + } + Assert.Close(n2 * hnuWeighted, power, 1e-9, 0, "source power = N2 sum h nu / tau_rad(nu)"); + Assert.True(invTau > 0, "radiative rate"); + } + + [Test(Category = "gain", Reference = "homogeneously saturated amplifier, dP/dz = g0 P / (1 + P / P_sat): ln(G / G0) + (G - 1) P_in / P_sat = 0 (integrated law; Siegman, Lasers, ch. 7; Saleh and Teich, Fundamentals of Photonics, ch. 15)", Tolerance = "1e-4")] + public void Ase_SliceStep_IntegratesTheSaturatedAmplifierLaw() + { + double g0 = 200, L = 0.02, Pin = 0.3, Psat = 1.0; // g0 L = 4: G0 = e^4 + double G0 = Math.Exp(g0 * L), lo = 1, hi = G0; + for (int i = 0; i < 200; i++) { double G = 0.5 * (lo + hi); if (Math.Log(G / G0) + (G - 1) * Pin / Psat > 0) hi = G; else lo = G; } + double Gref = 0.5 * (lo + hi); + int ns = 4000; double dz = L / ns, P = Pin; + for (int s = 0; s < ns; s++) + { + double g1 = g0 / (1 + P / Psat), Pm = AseModel.Slice(P, g1, 0, dz / 2, out _), g2 = g0 / (1 + Pm / Psat); + P = AseModel.Slice(P, g2, 0, dz, out _); + } + Assert.Close(Gref, P / Pin, 1e-4, 0, "saturated gain"); + // and the photon balance of one slice: stimulated = out - in - source dz + double o = AseModel.Slice(0.2, 30, 5, 1e-3, out double stim); + Assert.Close(o - 0.2 - 5e-3, stim, 1e-15, 1e-15, "stimulated part of a slice"); + } + + [Test(Category = "gain", Reference = "photon balance in the cw steady state: net absorbed pump photons = N2 V / tau + stimulated ASE photons", Tolerance = "0.5 %")] + public void Ase_CwSteadyState_ClosesThePhotonBalance_AndDepletesTheInversion() + { + // an end-pumped Yb:YAG rod; the signal mode (1-D cell) lies well inside the flat-top pump, so the inversion is uniform over the pump area + double a = 0.4e-3, L = 8e-3, wmode = 0.05 * a; + var med = GainMedium.Get("Yb:YAG"); + PumpSpec Pump() => new PumpSpec { Kind = "cw", PowerW = 250, WavelengthM = 940e-9, RadiusM = a, Profile = "tophat", Passes = 2 }; + GainCrystal Run(bool ase, out double absorbedW) + { + var gc = new GainCrystal(med, L, 24, Pump(), wmode, null, null); + if (ase) gc.AseDepletion = new AseModel(gc, new AseSpec { Enabled = true, SolidAngleSr = 1.0 }, 1.82, 2 * Math.PI * C / 1030e-9); // 1 sr: the escape cone of YAG, the upper bound + gc.PumpCw(12 * med.LifetimeS, 600, false); + double tt = 0.05 * med.LifetimeS; gc.PumpCw(tt, 20, true); + absorbedW = gc.AbsorbedPumpJ.Sum() / tt; + return gc; + } + var g0 = Run(false, out double abs0); + var g1 = Run(true, out double abs1); + double hvp = H * C / 940e-9, V = Math.PI * a * a * g1.Dz, areaRatio = Math.PI * a * a / g1.CellArea[0]; + double nPhotons = abs1 / hvp, decay = g1.N2.Sum() * V / med.LifetimeS; + var rates = g1.AseDepletion.Rates(g1.N2, g1.Nt); + double stim = g1.AseDepletion.LastStimulatedPhotonsPerS * areaRatio; // the column of the mode stands for the whole pump area + Assert.Greater(stim / nPhotons, 0.02, "ASE matters in this case (fraction of the absorbed pump photons)"); + double decay0 = g0.N2.Sum() * V / med.LifetimeS; + Assert.Close(abs0 / hvp, decay0, 5e-3, 0, "photon balance without ASE"); + Assert.Close(nPhotons, decay + stim, 5e-3, 0, "photon balance with ASE"); + Assert.Less(g1.N2.Sum(), 0.97 * g0.N2.Sum(), "ASE lowers the stored inversion"); + Assert.Greater(abs1, abs0, "less bleaching of the pump with a lower inversion"); + } + + [Test(Category = "gain", Reference = "limits: no solid angle gives no depletion; the depletion grows with the solid angle", Tolerance = "1e-6")] + public void Ase_Depletion_VanishesWithTheSolidAngle_AndGrowsWithIt() + { + double a = 0.4e-3, L = 8e-3; var med = GainMedium.Get("Yb:YAG"); + double Stored(double sr) + { + var pump = new PumpSpec { Kind = "cw", PowerW = 250, WavelengthM = 940e-9, RadiusM = a, Profile = "tophat", Passes = 2 }; + var gc = new GainCrystal(med, L, 16, pump, a * Math.Sqrt(2), null, null); + if (sr >= 0) gc.AseDepletion = new AseModel(gc, new AseSpec { Enabled = true, SolidAngleSr = sr }, 1.82, 2 * Math.PI * C / 1030e-9); + gc.PumpCw(12 * med.LifetimeS, 400, false); + return gc.N2.Sum(); + } + double none = Stored(-1), tiny = Stored(1e-12), small = Stored(0.01), large = Stored(0.3); + Assert.Close(none, tiny, 1e-6, 0, "vanishing solid angle"); + Assert.Less(small, none, "depletes"); Assert.Less(large, small, "more with a larger angle"); + } + + [Test(Category = "gain", Reference = "the element reports the ASE leaving the signal mode and the depletion; the energy budget of the pulse still closes", Tolerance = "1e-6")] + public void AseModel_OnAnAmplifier_ReportsThePedestal_AndTheBudgetCloses() + { + RunResult Amp(bool ase) => Go(Sys(O("id", "amp", "type", "gain", "medium", "Yb:YAG", "lengthM", 0.008, "slices", 16, "modeRadiusM", 0.4e-3 * Math.Sqrt(2), "spm", false, + "pump", O("kind", "cw", "powerW", 250, "wavelengthM", 940e-9, "radiusM", 0.4e-3, "profile", "tophat", "passes", 2), "thermalLens", false, + "repRateHz", 1e5, "aseModel", ase)), Spec(Gauss(1030e-9, 1e-12, 1e-8), Grid(1024, 40e-12))); + var r0 = Amp(false); var r1 = Amp(true); + var v = r1.Records[0].Values; + Assert.Greater(v["aseForwardPowerW"], 0, "forward ASE in the mode"); Assert.Greater(v["aseBackwardPowerW"], 0, "backward ASE"); + Assert.Greater(v["aseDepletionFraction"], 0, "depletion"); Assert.Less(v["aseDepletionFraction"], 1, "less than the absorbed pump"); + Assert.Greater(v["asePedestalEnergyFraction"], 0, "pedestal strength"); + Assert.Less(r1.Records[0].Values["inversionFractionBefore"], r0.Records[0].Values["inversionFractionBefore"], "ASE lowers the inversion the pulse sees"); + Assert.Less(r1.Output.TotalEnergyJ(), r0.Output.TotalEnergyJ(), "and the extracted energy"); + Assert.False(r0.Records[0].Values.ContainsKey("aseForwardPowerW"), "off by default"); + Assert.Close(r1.Input.TotalEnergyJ() - r1.Output.TotalEnergyJ(), r1.Records[0].LossJ.Values.Sum(), 1e-6, 1e-21, "budget closes"); + } + + [Test(Category = "gain", Reference = "Koechner, Solid-State Laser Engineering, ch. 7: the beam average of the four-lobed pattern, D = (1 - ) / 4, delta = C r^2 / r0^2", Tolerance = "1e-6 (quadrature on the grid; the Gaussian is well inside the grid)")] + public void StressPattern_BeamAverage_EqualsTheLumpedDepolarisation_AndHasFourLobes() + { + double w = 0.8e-3, r0 = 3e-3, Cst = 3.0; int N = 256; double half = 4.5e-3; + var basis = new CartesianBasis(N, N, half, half); int M = basis.M; + var ex = new Complex[M]; var ey = new Complex[M]; + for (int m = 0; m < M; m++) ex[m] = Math.Exp(-(basis.X[m] * basis.X[m] + basis.Y[m] * basis.Y[m]) / (w * w)); + double e0 = ex.Sum(z => z.Real * z.Real); + GainElement.ApplyStressPattern(ex, ey, 1, basis, Cst, r0); + double exE = ex.Sum(z => z.Real * z.Real + z.Imaginary * z.Imaginary), eyE = ey.Sum(z => z.Real * z.Real + z.Imaginary * z.Imaginary); + Assert.Close(e0, exE + eyE, 1e-13, 0, "unitary: energy kept"); + Assert.Close(GainElement.StressDepolarization(Cst, w, r0), eyE / (exE + eyE), 1e-6, 0, "beam average"); + // lobes: no y on the axes, the maximum on the diagonals, the sign alternates from quadrant to quadrant + double OnAxis = 0, Diag = 0; int iq1 = -1, iq2 = -1; + for (int m = 0; m < M; m++) + { + double x = basis.X[m], y = basis.Y[m]; + if (Math.Abs(y) < 1e-9 && Math.Abs(x) > 0 && Math.Abs(x) < 1.5e-3) OnAxis = Math.Max(OnAxis, ey[m].Magnitude); + if (Math.Abs(x - y) < 1e-9 && x > 0.5e-3 && x < 1.5e-3) Diag = Math.Max(Diag, ey[m].Magnitude); + if (iq1 < 0 && x > 0.5e-3 && y > 0.5e-3 && Math.Abs(x - y) < basis.Dx * 0.6) iq1 = m; + if (iq2 < 0 && x < -0.5e-3 && y > 0.5e-3 && Math.Abs(x + y) < basis.Dx * 0.6) iq2 = m; + } + Assert.Less(OnAxis, 1e-9 * Diag, "no depolarised light on the x axis (E along a principal axis)"); + Assert.Greater(Diag, 1e-3, "four lobes on the diagonals"); + Assert.Less((ey[iq1] * Complex.Conjugate(ey[iq2])).Real, 0, "adjacent lobes have opposite sign"); + } + + [Test(Category = "gain", Reference = "the pattern on a cartesian grid carries the same beam-averaged loss as the lumped depolarisation reported by the element", Tolerance = "5 % at the exit radius (the gain is not uniform over the beam)")] + public void StressPattern_InAGainElement_AgreesWithTheLumpedValue_AndTheProfileCarriesY() + { + RunResult Run(string model) => Go(Sys(O("id", "g", "type", "gain", "medium", "Nd:YAG", "lengthM", 0.02, "slices", 6, "beam", O("kind", "gaussian", "w0M", 0.5e-3), "spm", false, + "pump", O("kind", "cw", "powerW", 400, "wavelengthM", 808e-9, "radiusM", 1.5e-3, "profile", "tophat"), + "thermalLens", O("rodRadiusM", 2.5e-3, "heatFraction", 0.3, "stress", true), + "transverse", model == "cartesian" ? O("model", "cartesian", "nx", 64, "ny", 64, "halfWidthM", 2.5e-3) : O("model", "radial", "points", 48, "radiusM", 2.5e-3))), + Spec(Gauss(1064e-9, 1e-10, 1e-9))); + var r = Run("cartesian"); var v = r.Records[0].Values; + Assert.True(v.ContainsKey("depolarizationFractionPattern"), "pattern applied"); + // the lumped value uses the beam radius at the middle of the rod, the pattern the beam at the exit (the thermal lens has changed it): + // D scales as w^4 for a small beam, so compare with the lumped formula at the radius of the output beam + var rec = r.Records[0]; double wOut = rec.BeamRadiusXM; + double dOut = GainElement.StressDepolarization(v["stressRetardationRad"], wOut, 2.5e-3); + Assert.Close(dOut, v["depolarizationFractionPattern"], 0.05, 0, "pattern average against the lumped formula at the exit radius"); + Assert.Close(v["depolarizationFraction"], v["depolarizationFractionPattern"], 0.35, 0, "and the mid-rod lumped value is the same order"); + double ey = r.Output.Ay.Sum(z => z.Real * z.Real + z.Imaginary * z.Imaginary), ex = r.Output.Ax.Sum(z => z.Real * z.Real + z.Imaginary * z.Imaginary); + Assert.Close(v["depolarizationFractionPattern"], ey / (ex + ey), 1e-6, 0, "energy in y"); + Assert.True(r.Output.Profile != null && r.Output.Profile.DataY != null, "the carried profile has its own y shape"); + var rad = Run("radial"); // no 2-D basis: the lumped rotation, as before + Assert.False(rad.Records[0].Values.ContainsKey("depolarizationFractionPattern"), "radial grid: lumped"); + Assert.Close(rad.Records[0].Values["depolarizationFraction"], v["depolarizationFraction"], 0.02, 0, "same lumped number (the radius at mid-rod differs slightly between the two bases)"); + } + } +} diff --git a/tests/UltrafastEngine.Tests/BeamSourceTests.cs b/tests/UltrafastEngine.Tests/BeamSourceTests.cs index dbfe2e6..9ce329e 100644 --- a/tests/UltrafastEngine.Tests/BeamSourceTests.cs +++ b/tests/UltrafastEngine.Tests/BeamSourceTests.cs @@ -134,6 +134,50 @@ public void MeasuredNearField_FromAnNpz_PropagatesLikeTheBeamItSampled() finally { Directory.Delete(dir, true); } } + [Test(Reference = "a measured scene of one row (what a '1-D' scene saved at y = (j - ny/2) dx with ny = 1 is: its row is at y = -dx/2) reproduces on a grid with ny = 1, 4 and 128 the same x profile as a full map: radius w of the Gaussian and w sqrt(2) after one Rayleigh length", Tolerance = "radius 1 %, w(z_R) 2 %")] + public void MeasuredScene_OfOneRow_IsConstantAlongY_OnGridsWithAnyNy() + { + string dir = Path.Combine(Path.GetTempPath(), "uf-nearfield-ny-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); + try + { + double w = 0.5e-3, zr = Math.PI * w * w / Lam, dx = 6e-3 / 200; int n = 200; + var ax = Enumerable.Range(0, n).Select(i => (i - n / 2) * dx).ToArray(); + var fl = ax.Select(x => 7 + 100 * Math.Exp(-2 * x * x / (w * w))).ToArray(); + string f = Path.Combine(dir, "row.npz"); + NpzWriter.Write(f, new[] { new NdArray("x_m", ax, n), new NdArray("y_m", new[] { -dx / 2 }, 1), new NdArray("fluence", fl, n, 1) }); + var ini = O("kind", "measured", "file", f, "background", "auto"); + foreach (int ny in new[] { 1, 4, 128 }) + { + JsonObject G(bool init) { var g = O("model", "cartesian", "nx", 128, "ny", ny, "halfWidthM", 3e-3, "absorbingBoundary", true, "spectralSamples", 4); if (init) g.Set("initialProfile", ini); return g; } + var r = Go(Sys(Source(G(true)), Free("z", zr, G(false))), Pulse()); + Assert.Close(w, Rec(r, "src").BeamRadiusXM, 0.01, 0, "radius of the row, ny = " + ny); + Assert.Close(w * Math.Sqrt(2), Rec(r, "z").BeamRadiusXM, 0.02, 0, "w(z_R), ny = " + ny); + } + } + finally { Directory.Delete(dir, true); } + } + + [Test(Reference = "a mask file of one row is constant along y: a transmission of 0.5 everywhere passes 0.25 of the energy on a grid with ny = 1 and ny = 4", Tolerance = "1e-9")] + public void MaskFile_OfOneRow_IsConstantAlongY() + { + string dir = Path.Combine(Path.GetTempPath(), "uf-mask-ny-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); + try + { + double dx = 6e-3 / 128; int n = 128; + var ax = Enumerable.Range(0, n).Select(i => (i - n / 2) * dx).ToArray(); + string f = Path.Combine(dir, "mask.npz"); + NpzWriter.Write(f, new[] { new NdArray("x_m", ax, n), new NdArray("y_m", new[] { -dx / 2 }, 1), new NdArray("amplitude", Enumerable.Repeat(0.5, n).ToArray(), n, 1) }); + foreach (int ny in new[] { 1, 4 }) + { + var g = Line(128, 3e-3, O("kind", "gaussian", "radiusM", 0.5e-3)); g.Set("ny", ny); + var gm = Line(128, 3e-3); gm.Set("ny", ny); + var r = Go(Sys(Source(g), O("id", "m", "type", "mask", "kind", "file", "file", f), Free("z", 0.0, gm)), Pulse()); + Assert.Close(0.25, Rec(r, "m").Values["transmission"], 1e-9, 0, "energy transmission, ny = " + ny); + } + } + finally { Directory.Delete(dir, true); } + } + [Test(Reference = "Fresnel zone plate: amplitude on the axis 2 N_open U0, intensity 4 N_open^2 I0; binary phase plate 16 N_open^2 I0 (Born & Wolf 8.8)", Tolerance = "5 %")] public void ZonePlate_FocusesAtItsFocalLength_AmplitudeAndBinaryPhase() { diff --git a/tests/UltrafastEngine.Tests/BeamTrackingTests.cs b/tests/UltrafastEngine.Tests/BeamTrackingTests.cs index 25254dd..1b8b8dc 100644 --- a/tests/UltrafastEngine.Tests/BeamTrackingTests.cs +++ b/tests/UltrafastEngine.Tests/BeamTrackingTests.cs @@ -53,7 +53,7 @@ public void ThermalLens_MeasuredFocalLength_FocusesTheTrackedBeamLikeAThinLens() "thermalLens", O("focalLengthM", f)), O("id", "gap", "type", "gap", "lengthM", d - L / 2 / n, "material", "Vacuum")); var r = Go(sys, Tracked(w0)); - Assert.Close(f, Rec(r, "g").Values["trackedThermalLensFocalLengthM"], 1e-12, 0, "the given focal length is used"); + Assert.Close(f, Rec(r, "g").Values["trackedThermalLensEffectiveFocalLengthM"], 1e-9, 0, "the given focal length is the effective one"); Assert.Close(w1, Rec(r, "gap").Values["trackedBeamRadiusXM"], 0.002, 0, "waist radius"); Assert.True(Math.Abs(Rec(r, "gap").Values["trackedCurvatureXM"]) > 100, "flat wavefront at the waist"); } @@ -82,6 +82,113 @@ JsonObject Rod(bool spatial) Assert.Less(wt, 0.5e-3, "the thermal lens converges the beam"); } + // ----------------------------------------------------------------------------- the thermal lens as a graded-index duct + + private static System.Numerics.Complex Abcd(System.Numerics.Complex q, double a, double b, double c, double d) => (a * q + b) / (c * q + d); + private static double Radius(System.Numerics.Complex q) => Math.Sqrt(Lam / (Math.PI * (-1 / q).Imaginary)); + + [Test(Reference = "quadratic-index medium n(r) = n0 (1 - g² r²/2): ABCD cos(gL), sin(gL)/(n0 g) (Saleh & Teich, ch. 1)", Tolerance = "1e-12")] + public void Duct_Matrix_IsTheQuadraticIndexResult_AndTheThinLensLimit() + { + double n0 = 1.82; + foreach (var (L, g) in new[] { (0.1, 3.0), (0.37, 1.1), (0.02, 20.0) }) + { + var m = Optics.BeamState.DuctMatrix(L, n0, g * g); + Assert.Close(Math.Cos(g * L), m[0], 1e-12, 1e-14, "A"); + Assert.Close(Math.Sin(g * L) / (n0 * g), m[1], 1e-12, 1e-14, "B"); + Assert.Close(-n0 * g * Math.Sin(g * L), m[2], 1e-12, 1e-14, "C"); + Assert.Close(m[0], m[3], 0, 1e-15, "D = A"); + Assert.Close(1, m[0] * m[3] - m[1] * m[2], 1e-12, 0, "determinant"); + } + // g² < 0 (negative dn/dT hosts): hyperbolic functions + double h = 2.0, Ln = 0.2; var mn = Optics.BeamState.DuctMatrix(Ln, n0, -h * h); + Assert.Close(Math.Cosh(h * Ln), mn[0], 1e-12, 0, "cosh"); Assert.Close(Math.Sinh(h * Ln) / (n0 * h), mn[1], 1e-12, 0, "sinh"); + Assert.Close(n0 * h * Math.Sinh(h * Ln), mn[2], 1e-12, 0, "C (defocusing)"); + // thin limit gL -> 0: a gap L/n0 and a lens of power 1/f = n0 g² L, to O((gL)²); and continuity across the series switch + double L0 = 1e-3, p = 0.7, g2 = p / (n0 * L0); // power p, gL = 0.02 + var t = Optics.BeamState.DuctMatrix(L0, n0, g2); + Assert.Close(-p, t[2], 1e-3, 0, "thin-lens power -1/f"); + Assert.Close(L0 / n0, t[1], 1e-3, 0, "reduced length"); + foreach (double g2s in new[] { 0.99, 1.01 }) // (gL)² = 0.99e-8 (series) and 1.01e-8 (closed form): both the exact functions + { + double Ls = 1e-4, gs = Math.Sqrt(g2s); var ms = Optics.BeamState.DuctMatrix(Ls, n0, g2s); + Assert.Close(Math.Cos(gs * Ls), ms[0], 1e-12, 0, "A near the switch"); Assert.Close(Math.Sin(gs * Ls) / (n0 * gs), ms[1], 1e-12, 0, "B near the switch"); + Assert.Close(-n0 * gs * Math.Sin(gs * Ls), ms[2], 1e-8, 0, "C near the switch"); + } + // an effective focal length (focusing and defocusing) maps to a duct whose -1/C is that focal length + foreach (double fe in new[] { 0.15, 2.0, -0.3, -5.0 }) + { + double thin = Optics.BeamState.ThinPowerForEffectiveFocalLength(0.1, n0, fe, out bool capped); + var me = Optics.BeamState.DuctMatrix(0.1, n0, thin / (n0 * 0.1)); + Assert.True(!capped, "not capped"); Assert.Close(fe, -1 / me[2], 1e-9, 0, "effective focal length " + fe); + } + Optics.BeamState.ThinPowerForEffectiveFocalLength(0.1, n0, 0.01, out bool cap2); + Assert.True(cap2, "a focal length shorter than a quarter pitch gives is flagged"); + var zero = Optics.BeamState.DuctMatrix(0.1, n0, 0); + Assert.Close(0.1 / n0, zero[1], 1e-15, 0, "g = 0 is a gap"); Assert.Close(0, zero[2], 0, 1e-15, "no power"); + } + + [Test(Reference = "slices of a graded-index medium: N thin lenses + gaps converge to the duct as 1/N²", Tolerance = "1e-3 at N = 40, ratio 4 for 2N")] + public void Duct_ManyThinSlices_ConvergeToTheDuct_WithSecondOrderError() + { + double n0 = 1.82, L = 0.1, f = 0.15; // gL = sqrt(L / (n0 f)) = 0.6 + var q0 = new System.Numerics.Complex(0, Math.PI * Math.Pow(0.5e-3, 2) / Lam); + var bd = new Optics.BeamState(q0, q0, Lam); bd.Duct(L, n0, 1 / (n0 * f * L), 1 / (n0 * f * L)); + double Err(int N) + { + var b = new Optics.BeamState(q0, q0, Lam); + for (int i = 0; i < N; i++) { b.Propagate(L / N / 2, n0); b.Lens(f * N, f * N); b.Propagate(L / N / 2, n0); } + return Math.Abs(b.RadiusX / bd.RadiusX - 1); + } + Assert.Less(Err(40), 1e-3, "40 thin slices"); + Assert.Close(4, Err(20) / Err(40), 0.05, 0, "error falls as 1/N²"); + Assert.Close(L, bd.PathM, 1e-15, 0, "path length"); Assert.Close(n0, bd.MediumIndex, 0, 0, "medium index"); + } + + [Test(Category = "gain", Reference = "ABCD matrix of a quadratic-index duct (gL = 0.6): not the thin lens at the centre", Tolerance = "1e-9")] + public void ThermalLens_ThickRod_TrackedBeamIsTheDuctResult() + { + // A 0.1 m rod, gL = 0.6, on a collimated 0.5 mm beam. A measured focal length is the effective one, -1/C = 1 / (n g sin gL): the tracked q + // after the rod and 5 mm of vacuum follows the exact duct matrix, and the thin lens at the centre of the rod would be off by about 1 %. + double w0 = 0.5e-3, L = 0.1, gap = 5e-3, gL = 0.6; + double n = MaterialLibrary.BuiltIn("YAG").IndexAt(2 * Math.PI * C / Lam); + double g = gL / L, f = 1 / (n * g * Math.Sin(gL)); + var sys = Sys(O("id", "g", "type", "gain", "medium", "Yb:YAG", "lengthM", L, "inversion", O("fraction", 0.0), "spm", false, "thermalLens", O("focalLengthM", f)), + O("id", "gap", "type", "gap", "lengthM", gap, "material", "Vacuum")); + var r = Go(sys, Tracked(w0)); + var q = new System.Numerics.Complex(0, Math.PI * w0 * w0 / Lam); + var qd = Abcd(q, Math.Cos(g * L), Math.Sin(g * L) / (n * g), -n * g * Math.Sin(g * L), Math.Cos(g * L)) + gap; + Assert.Close(Radius(qd), Rec(r, "gap").Values["trackedBeamRadiusXM"], 1e-9, 0, "radius after the duct"); + var qt = Abcd(Abcd(Abcd(q, 1, L / 2 / n, 0, 1), 1, 0, -1 / f, 1), 1, L / 2 / n, 0, 1) + gap; + Assert.Greater(Math.Abs(Radius(qt) / Radius(qd) - 1), 0.005, "the thin lens at the centre would differ (this test can tell them apart)"); + var v = Rec(r, "g").Values; + Assert.Close(f, v["trackedThermalLensEffectiveFocalLengthM"], 1e-9, 0, "effective focal length -1/C is the given one"); + Assert.Close(1 / (n * g * g * L), v["trackedThermalLensFocalLengthM"], 1e-9, 0, "thin-lens equivalent 1/(n g² L)"); + } + + [Test(Category = "gain", Reference = "N round trips of a regenerative amplifier = N x passesPerRoundTrip duct passes", Tolerance = "1e-12")] + public void Regen_ThermalLens_OnTheTrackedBeam_EqualsTheSamePassesOfAMultipass() + { + double w0 = 0.4e-3; + JsonObject Amp(string geometry, params object[] extra) + { + var all = new object[] { "id", "amp", "type", "amplifier", "geometry", geometry, "medium", "Yb:YAG", "lengthM", 0.004, "modeRadiusM", w0, "inversion", O("fraction", 0.1), "spm", false, + "pump", O("kind", "cw", "powerW", 40, "wavelengthM", 940e-9, "radiusM", 0.45e-3, "passes", 2) }; + return O(all.Concat(extra).ToArray()); + } + var tl = O("track", true, "passGapM", 0.3, "rodRadiusM", 2e-3); + var regen = Go(Sys(Amp("regenerative", "roundTrips", 3, "passesPerRoundTrip", 2, "thermalLens", tl)), Tracked(w0)); + var multi = Go(Sys(Amp("multipass", "passes", 6, "thermalLens", tl)), Tracked(w0)); + var rv = Rec(regen, "amp").Values; var mv = Rec(multi, "amp").Values; + Assert.Close(mv["trackedBeamRadiusXM"], rv["trackedBeamRadiusXM"], 1e-12, 0, "tracked radius after 6 passes"); + Assert.Close(mv["trackedThermalLensFocalLengthM"], rv["trackedThermalLensFocalLengthM"], 1e-12, 0, "thin-lens equivalent"); + Assert.True(rv.ContainsKey("trackedThermalLensEffectiveFocalLengthM"), "effective focal length is reported"); + var none = Go(Sys(Amp("regenerative", "roundTrips", 3, "passesPerRoundTrip", 2)), Tracked(w0)); + Assert.Greater(Math.Abs(rv["trackedBeamRadiusXM"] / Rec(none, "amp").Values["trackedBeamRadiusXM"] - 1), 1e-3, "the lens changes the tracked beam"); + var auto = Go(Sys(Amp("regenerative", "roundTrips", "auto", "thermalLens", tl)), Tracked(w0)); + Assert.True(Rec(auto, "amp").Notes.Any(x => x.Contains("explicit roundTrips")), "auto round trips: no tracking, with a note"); + } + // ================================================================================= spatio-temporal couplings private const double Lpm = 1200; diff --git a/tests/UltrafastEngine.Tests/CatalogueSnapshotTests.cs b/tests/UltrafastEngine.Tests/CatalogueSnapshotTests.cs new file mode 100644 index 0000000..749f316 --- /dev/null +++ b/tests/UltrafastEngine.Tests/CatalogueSnapshotTests.cs @@ -0,0 +1,161 @@ +// CatalogueSnapshotTests.cs -- pins every number the material library hands to the engine. +// +// What it does: evaluates every built-in material, gas and Raman model (index at 24 wavelengths, n2, Raman fraction, h(t) at 80 +// times, rotational-Raman lines, ionisation data, gas data) and compares the result with Data/catalogue-snapshot.txt. +// Why: WP3 moved these numbers from C# tables into catalogue files (src/UltrafastEngine.Core/Data/materials). That move had to be a pure +// refactor, so the snapshot was written from the old tables first and the new loader reproduces it to 1e-15. +// A value is changed on purpose in two steps: edit the catalogue file, then regenerate the snapshot +// (ULTRAFAST_WRITE_SNAPSHOT=1) and say in the commit which value moved and why. +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using UltrafastEngine.Materials; +using UltrafastEngine.Models; + +namespace UltrafastEngine.Tests +{ + [TestArea("Material catalogue: values pinned, provenance, user catalogues")] + public class CatalogueSnapshotTests + { + private static string SnapshotPath() => Path.Combine(SchemaTests.RepoRoot() ?? ".", "tests", "UltrafastEngine.Tests", "Data", "catalogue-snapshot.txt"); + private static string R(double v) => v.ToString("R", CultureInfo.InvariantCulture); + + internal static SortedDictionary Dump() + { + var d = new SortedDictionary(StringComparer.Ordinal); + var c = CultureInfo.InvariantCulture; + var lambdaM = Enumerable.Range(0, 24).Select(i => (0.4 + 0.07 * i) * 1e-6).ToArray(); + var omega = lambdaM.Select(l => 2 * Math.PI * 2.99792458e8 / l).ToArray(); + var t = Enumerable.Range(0, 80).Select(i => i * 25e-15).ToArray(); + + void Material(string key, Material m) + { + var n = new double[omega.Length]; + m.Index.Evaluate(omega, n); + for (int i = 0; i < n.Length; i++) d[key + ".n[" + i + "]"] = R(n[i]); + d[key + ".n2"] = R(m.N2); d[key + ".fR"] = R(m.RamanFraction); d[key + ".sigma"] = R(m.KerrAnisotropy); + d[key + ".isGas"] = m.IsGas.ToString(); d[key + ".indexReference"] = m.IndexReference; + if (m.Raman != null) + { + var h = m.Raman.Evaluate(t); + for (int i = 0; i < h.Length; i++) d[key + ".h[" + i + "]"] = R(h[i]); + if (m.Raman is IVectorDelayedResponse v) + { + d[key + ".hAnisoFraction"] = R(v.AnisotropicFraction); + var hb = v.EvaluateAnisotropic(t); + for (int i = 0; i < hb.Length; i += 4) d[key + ".hb[" + i + "]"] = R(hb[i]); + } + } + if (m.Rotational is RotationalRamanResponse r) + { + int k = 0; + foreach (var (om, w, g) in r.Lines) { d[key + ".rot[" + k + "]"] = R(om) + " " + R(w) + " " + R(g); k++; } + } + } + + foreach (var name in MaterialLibrary.BuiltInNames) + { + Material("mat:" + name, MaterialLibrary.BuiltIn(name)); + var ion = SolidIonizationData.For(name); + if (ion != null) d["ion:" + name] = string.Join(" ", new[] { ion.BandGapEv, ion.EffectiveMass, ion.NeutralDensityM3, ion.CollisionTimeS, ion.RecombinationTimeS }.Select(R)); + } + foreach (var sp in new[] { "He", "Ne", "Ar", "Kr", "Xe", "N2", "O2", "CO2", "H2", "Air" }) + { + Material("gas:" + sp + "@2.5bar310K", MaterialLibrary.Gas(sp, 2.5, 310)); + Material("gas:" + sp + "@1bar293K", MaterialLibrary.Gas(sp, 1.0, 293.15)); + } + foreach (var g in GasData.Table.Values) + d["gasdata:" + g.Species] = string.Join(" ", new[] { g.IonizationEv, g.L, g.ZEff, g.N2Electronic, g.RotB, g.RotD, g.AnisotropyA3, g.GEven, g.GOdd, g.T2At1Bar }.Select(R)); + foreach (var kv in GasN2Table()) d["gasn2:" + kv.Key] = R(kv.Value); + foreach (var kv in GasIndex.Coefficients) d["gasindex:" + kv.Key] = string.Join(" ", kv.Value.Select(R)); + + var raman = new Dictionary + { + ["silica"] = new DampedOscillatorResponse(12.2e-15, 32e-15), + ["linAgrawal"] = new LinAgrawalRamanResponse(), + ["hollenbeck"] = new HollenbeckCantrellRamanResponse(), + ["orientational"] = new OrientationalResponse(0.2e-12, 1.6e-12) + }; + foreach (var kv in raman) + { + var h = kv.Value.Evaluate(t); + for (int i = 0; i < h.Length; i++) d["raman:" + kv.Key + ".h[" + i + "]"] = R(h[i]); + } + foreach (var kind in new[] { "silica", "blowWood", "hollenbeck", "linAgrawal", "other" }) d["ramanfraction:" + kind] = R(MaterialLibrary.DefaultRamanFraction(kind)); + // gain media (cross sections on a wavelength comb, scalars), crystals (principal indices, tensor, scalars), Ciddor air + var wl = Enumerate(40, 0.45e-6, 1.12e-6); + foreach (var gname in UltrafastEngine.Gain.GainMedium.Names) + foreach (var temp in new[] { 293.15, 350.0 }) + { + var g = UltrafastEngine.Gain.GainMedium.Get(gname, temp); string k = "gain:" + gname + "@" + R(temp); + d[k + ".scalars"] = string.Join(" ", new[] { g.LifetimeS, g.DopingDensityM3, g.ZeroLineM, g.ThermalConductivity, g.DnDT, g.ThermalExpansionPerK, g.StressCr, g.StressCphi, g.TemperatureK }.Select(R)); + d[k + ".text"] = g.Name + "|" + g.Host + "|" + g.Notes.Count; + for (int i = 0; i < wl.Length; i++) + { + double w = UltrafastEngine.Gain.GainMedium.Omega(wl[i]); + d[k + ".s[" + i + "]"] = R(g.SigmaE(w)) + " " + R(g.SigmaA(w)) + " " + R(g.SaturationFluence(w)); + } + } + var lam = Enumerate(8, 0.9e-6, 1.5e-6); + foreach (var cname in UltrafastEngine.Crystals.CrystalLibrary.Names) + foreach (var temp in new[] { 20.0, 80.0 }) + { + var cx = UltrafastEngine.Crystals.CrystalLibrary.Get(cname, temp); string k = "crystal:" + cname + "@" + R(temp); + d[k + ".text"] = cx.Name + "|" + cx.OpticalClass + "|" + cx.PointGroup + "|" + cx.TemperatureDependent + "|" + cx.Notes.Count; + d[k + ".scalars"] = string.Join(" ", new[] { cx.N2, cx.ValidMinUm, cx.ValidMaxUm, cx.DeffOverrideMPerV, cx.TemperatureC }.Select(R)); + if (cx.D != null) for (int a = 0; a < 3; a++) d[k + ".d" + a] = string.Join(" ", Enumerable.Range(0, 6).Select(b => R(cx.D[a, b]))); + var om = lam.Select(l => 2 * Math.PI * 2.99792458e8 / l).ToArray(); + foreach (var (axis, ix) in new[] { ("x", cx.Nx), ("y", cx.Ny), ("z", cx.Nz) }) { var n = new double[om.Length]; ix.Evaluate(om, n); d[k + ".n" + axis] = string.Join(" ", n.Select(R)); } + } + foreach (var (tc, p, rh, co2) in new[] { (20.0, 101325.0, 0.0, 450.0), (15.0, 101325.0, 0.5, 400.0), (25.0, 90000.0, 0.4, 450.0) }) + { + var ai = new CiddorAirIndex(tc, p, rh, co2); var n = new double[omega.Length]; ai.Evaluate(omega, n); + string k = "ciddor:" + R(tc) + "," + R(p) + "," + R(rh) + "," + R(co2); + d[k + ".xw"] = R(ai.WaterVaporMoleFraction); + for (int i = 0; i < n.Length; i++) d[k + ".n[" + i + "]"] = R(n[i]); + } + d["const:BaF2N2PerW"] = R(MaterialLibrary.BaF2N2PerW); d["const:BaF2Sigma"] = R(MaterialLibrary.BaF2Sigma); + d["const:CS2"] = string.Join(" ", new[] { MaterialLibrary.CS2N2TotalPerW, MaterialLibrary.CS2OrientationalFraction, MaterialLibrary.CS2RiseS, MaterialLibrary.CS2DecayS }.Select(R)); + return d; + } + + private static double[] Enumerate(int n, double lo, double hi) => Enumerable.Range(0, n).Select(i => lo + (hi - lo) * i / (n - 1)).ToArray(); + + private static IEnumerable> GasN2Table() => MaterialLibrary.GasN2PerBar.OrderBy(k => k.Key, StringComparer.Ordinal); + + [Test(Category = "materials", Reference = "the values of the catalogue files equal the old C# tables", Tolerance = "1e-15 relative")] + public void Catalogue_ReproducesTheValuesPinnedBeforeTheMove() + { + var path = SnapshotPath(); + var now = Dump(); + if (Environment.GetEnvironmentVariable("ULTRAFAST_WRITE_SNAPSHOT") == "1") + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllLines(path, now.Select(kv => kv.Key + "\t" + kv.Value)); + Console.WriteLine("wrote " + path + " (" + now.Count + " entries)"); + return; + } + if (!File.Exists(path)) { Assert.Skip("snapshot not found: " + path); } + var old = File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => l.Split(new[] { '\t' }, 2)).ToDictionary(p => p[0], p => p[1]); + Assert.Equal(old.Count, now.Count, "number of pinned entries"); + int bad = 0; string first = null; + foreach (var kv in old) + { + if (!now.TryGetValue(kv.Key, out var v)) { bad++; first = first ?? "missing " + kv.Key; continue; } + if (v == kv.Value) continue; + var a = kv.Value.Split(' '); var b = v.Split(' '); + bool same = a.Length == b.Length; + for (int i = 0; same && i < a.Length; i++) + { + if (double.TryParse(a[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) && double.TryParse(b[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var y)) + same = Math.Abs(x - y) <= 1e-15 * Math.Max(Math.Abs(x), Math.Abs(y)) + 1e-300; + else same = a[i] == b[i]; + } + if (!same) { bad++; first = first ?? kv.Key + ": pinned " + kv.Value + ", now " + v; } + } + Assert.True(bad == 0, bad + " pinned values differ; first: " + first); + } + } +} diff --git a/tests/UltrafastEngine.Tests/CatalogueTests.cs b/tests/UltrafastEngine.Tests/CatalogueTests.cs new file mode 100644 index 0000000..ba20b14 --- /dev/null +++ b/tests/UltrafastEngine.Tests/CatalogueTests.cs @@ -0,0 +1,336 @@ +// CatalogueTests.cs -- the material catalogue (WP3): file lint, published reference values, user catalogues, run notes. +// +// The values themselves are pinned in CatalogueSnapshotTests. Here: every catalogue file has the required structure (source, status, units in +// key names), the verified index sets reproduce published indices, a user file replaces a built-in quantity with a notice, and a run lists +// the approximate / unverified catalogue values it used (and nothing it did not use or that the user overrode). +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using UltrafastEngine.Materials; +using UltrafastEngine.Serialization; +using UltrafastEngine.Models; +using static UltrafastEngine.Tests.U; + +namespace UltrafastEngine.Tests +{ + [TestArea("Material catalogue: values pinned, provenance, user catalogues")] + public class CatalogueTests + { + private static readonly Regex UnitSuffix = new Regex(@"(S|M|Ev|J|K|Pa|C|M2|M3|M2PerW|PerCm|PerUm2|Um2|Um|A3|Percent|Ppm|PerBar273K|Bar|Cm2|PerK|WPerMK|PerPpm|JPerMolK|KgPerMol|GPerMol|GPerMolPerPpm|PerGPerMolFactor|PerV)$"); + private static readonly HashSet Dimensionless = new HashSet + { "a", "b", "c0", "value", "fa", "fb", "fc", "orbitalL", "zEff", "coefficients", "peakIntensities", "relativeUncertainty", "nuclearSpinWeightEven", "nuclearSpinWeightOdd", + "anisotropicFraction", "effectiveMassRatio", "borzsonyiCoefficients", "stressCr", "stressCphi", "correctionFactor", "polynomialCoefficients", "a", "b", "N2", "O2", "Ar" }; + + private static void Lint(string where, JsonValue v, List bad, bool unitParent = false) + { + if (v.Kind == JsonKind.Array) + { + int i = 0; + foreach (var item in v.AsArray()) { if (item.Kind == JsonKind.Object) Lint(where + "[" + i + "]", item, bad); i++; } + } + else if (v.Kind == JsonKind.Object) + foreach (var kv in v.AsObject()) + { + bool numeric = kv.Value.Kind == JsonKind.Number || (kv.Value.Kind == JsonKind.Array && kv.Value.AsArray().Any() && kv.Value.AsArray().All(x => x.Kind == JsonKind.Number)); + bool unit = UnitSuffix.IsMatch(kv.Key); + // the children of an object whose own key carries the unit (dPmPerV: {d22: ...}) are exempt + if (numeric && !unitParent && !Dimensionless.Contains(kv.Key) && !unit) + bad.Add(where + "." + kv.Key + ": a number needs its unit in the key name (or belongs in the dimensionless list of this test)"); + Lint(where + "." + kv.Key, kv.Value, bad, unit && kv.Value.Kind == JsonKind.Object); + } + } + + [Test(Category = "materials")] + public void CatalogueFiles_HaveSourceStatusAndUnitsInTheirKeys() + { + var bad = new List(Catalogue.Problems); + int files = 0; + foreach (var f in Catalogue.EmbeddedFiles()) + { + files++; + var o = JsonValue.Parse(f.Value).AsObject(); + var qs = o.GetObject("quantities"); + foreach (var q in qs) + { + var qo = q.Value.AsObject(); + if (qo.Has("ref")) continue; + if (qo.GetString("source", "").Trim().Length < 8) bad.Add(f.Key + ": " + q.Key + " has no real source"); + var st = qo.GetString("status", ""); + if (st != "verified" && st != "approximate" && st != "unverified") bad.Add(f.Key + ": " + q.Key + " status '" + st + "'"); + if (st != "verified" && !qo.Has("ref") && !qo.Has("relativeUncertainty") && !qo.Has("uncertainty") && q.Key != "composition") + bad.Add(f.Key + ": " + q.Key + " is " + st + " and states no uncertainty"); + Lint(f.Key + ":" + q.Key, qo, bad); + } + } + Assert.True(files >= 40, "catalogue files found: " + files); + Assert.True(bad.Count == 0, bad.Count + " problems:\n" + string.Join("\n", bad.Take(60))); + } + + [Test(Category = "materials")] + public void EveryBuiltInName_ResolvesToACatalogueEntry_AndBuilds() + { + foreach (var name in MaterialLibrary.BuiltInNames) + { + Assert.True(MaterialLibrary.IsBuiltIn(name), name); + var m = MaterialLibrary.BuiltIn(name); + Assert.True(m.Used.ContainsKey("index"), name + " does not record its catalogue index"); + } + Assert.Equal("FusedSilica", MaterialLibrary.Canonical("f_silica"), "alias"); + Assert.Equal("Ar", MaterialLibrary.Canonical("Argon"), "gas alias"); + Assert.Equal("Air", MaterialLibrary.Canonical(""), "empty name is air"); + } + + [Test(Category = "materials")] + public void VerifiedDispersionSets_RecordTheirCheck() + { + foreach (var e in Catalogue.Entries()) + { + var q = e.TryQ("index"); + if (q == null || q.Status != CatalogueStatus.Verified) continue; + string model = q.Text("model"); + if (model == "constant" || model == "ciddor") continue; + Assert.True(q.Text("verification", "").Length > 0, e.Id + ": a verified index records the check against its source page"); + Assert.True(q.Text("verification").StartsWith("agrees"), e.Id + ": " + q.Text("verification")); + } + } + + [Test(Category = "materials", Reference = "Malitson, JOSA 55, 1205 (1965): n_d = 1.45846 at 587.56 nm; Schott N-BK7 data sheet: n_d = 1.5168 at 587.56 nm; Dodge 1984 MgF2 n_o = 1.3777 at 589.3 nm", Tolerance = "2e-5 / 1e-4 / 1e-4")] + public void PublishedIndices_AreReproduced() + { + double N(string id, double um) { var n = new double[1]; CatalogueModels.Index(Catalogue.Get(id, "solid").Q("index")).Evaluate(new[] { 2 * Math.PI * 2.99792458e8 / (um * 1e-6) }, n); return n[0]; } + Assert.Close(1.45846, N("FusedSilica", 0.58756), 2e-5, 0, "fused silica at the helium d line"); + Assert.Close(1.5168, N("N-BK7", 0.58756), 1e-4, 0, "N-BK7 n_d"); + Assert.Close(1.3777, N("MgF2", 0.5893), 1e-4, 0, "MgF2 ordinary at the sodium D line"); + } + + [Test(Category = "materials", Reference = "Schlarb and Sugg, Phys. Status Solidi B 182, K91 (1994): measured n of TGG at 20 C (refractiveindex.info main/Tb3Ga5O12/Schlarb)", Tolerance = "1e-3")] + public void Tgg_Dispersion_FollowsTheMeasuredTable() + { + // the nine measured points (um, n relative to air); the fit's largest deviation is 6.6e-4. The previous two-point fit was 0.0106 away at 1.08 um. + double[,] table = { { 0.40466, 2.0178 }, { 0.43583, 2.0057 }, { 0.54607, 1.9765 }, { 0.63300, 1.9656 }, { 1.07981, 1.9434 }, { 1.08445, 1.9432 }, { 1.14091, 1.9420 }, { 1.15228, 1.9421 }, { 1.17668, 1.9415 } }; + var ix = CatalogueModels.Index(Catalogue.Get("TGG", "solid").Q("index")); + for (int i = 0; i < table.GetLength(0); i++) + { + var n = new double[1]; + ix.Evaluate(new[] { 2 * Math.PI * 2.99792458e8 / (table[i, 0] * 1e-6) }, n); + Assert.Close(table[i, 1], n[0], 1e-3, 0, "TGG at " + table[i, 0] + " um"); + } + Assert.True(Catalogue.Get("TGG", "solid").Q("index").Status == CatalogueStatus.Approximate, "a fit is approximate"); + } + + [Test(Category = "materials")] + public void UserCatalogue_ReplacesAQuantityWithANotice_AndCanAddAMaterial() + { + string dir = Path.Combine(Path.GetTempPath(), "uf-cat-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + File.WriteAllText(Path.Combine(dir, "FusedSilica.json"), @"{ // only the quantity that changes + ""id"": ""FusedSilica"", + ""quantities"": { ""n2"": { ""valueM2PerW"": 2.19e-20, ""source"": ""my own Z-scan, 2026"", ""status"": ""verified"" } } }"); + File.WriteAllText(Path.Combine(dir, "MyGlass.json"), @"{ ""id"": ""MyGlass"", ""kind"": ""solid"", ""listOrder"": 900, ""quantities"": { + ""index"": { ""model"": ""constant"", ""value"": 1.6, ""source"": ""vendor data sheet"", ""status"": ""verified"" }, + ""n2"": { ""valueM2PerW"": 5e-20, ""source"": ""vendor data sheet"", ""status"": ""approximate"", ""relativeUncertainty"": 0.5 } } }"); + Catalogue.Reload(dir); + var m = MaterialLibrary.BuiltIn("FusedSilica"); + Assert.Close(2.19e-20, m.N2, 1e-15, 0, "user n2"); + Assert.True(m.Used["n2"].IsUser, "the quantity knows it comes from a user file"); + Assert.Close(0.18, m.RamanFraction, 1e-15, 0, "the other quantities stay the built-in ones"); + Assert.True(Catalogue.Notices.Any(n => n.Contains("FusedSilica.n2")), "a notice names the replaced quantity"); + Assert.True(MaterialLibrary.IsBuiltIn("MyGlass"), "a new material from a user file"); + Assert.Close(1.6, MaterialLibrary.BuiltIn("MyGlass").IndexAt(2 * Math.PI * 2.99792458e8 / 800e-9), 1e-12, 0, "new material index"); + } + finally { Catalogue.Reload(null); Directory.Delete(dir, true); } + Assert.Close(2.6e-20, MaterialLibrary.BuiltIn("FusedSilica").N2, 1e-15, 0, "back to the built-in value"); + Assert.True(!MaterialLibrary.IsBuiltIn("MyGlass"), "the user material is gone after the reload"); + } + + private static string Nl(string material, JsonObject extra = null) + { + var sys = Sys(O("id", "nl", "type", "bulk", "material", material, "lengthM", 0.01, "beam", O("kind", "constant", "areaM2", 1e-8))); + var ex = extra ?? O(); + ex.Set("physics", O("raman", true)); + var res = Go(sys, Spec(Gauss(800e-9, 60e-15, 1e-7), ex)); + return string.Join("\n", res.Notes) + "\n" + res.SummaryJson().GetArray("unverifiedInputs").ToJson(false); + } + + [Test(Category = "materials")] + public void Run_ListsTheApproximateValuesItUsed_AndNotTheOnesTheUserOverrode() + { + string text = Nl("FusedSilica"); + Assert.True(text.Contains("APPROXIMATE catalogue value used: FusedSilica.n2"), "n2 of fused silica is approximate and used"); + Assert.True(text.Contains("FusedSilica.raman"), "the Raman response is used"); + Assert.True(!text.Contains("FusedSilica.index"), "the index is verified and is not listed"); + string over = Nl("FusedSilica", O("overrides", A(O("target", "material:FusedSilica", "property", "n2", "value", 2.2e-20)))); + Assert.True(!over.Contains("FusedSilica.n2"), "an overridden n2 is not reported"); + Assert.True(over.Contains("FusedSilica.raman"), "the other values still are"); + string gas = Nl("Ar"); + Assert.True(gas.Contains("Ar.n2"), "the electronic n2 of argon"); + Assert.True(!gas.Contains("Ar.rotation"), "an atomic gas has no rotational Raman"); + string air = Nl("Air"); + Assert.True(air.Contains("Air.dephasing.N2") && air.Contains("Air.rotation.O2"), "air reports the rotational data of N2 and O2"); + } + + [Test(Category = "materials")] + public void LinearRun_ReportsOnlyTheDispersionItUsed() + { + var sys = Sys(O("id", "g", "type", "bulk", "material", "YVO4", "lengthM", 0.01)); + var res = Go(sys, Spec(Gauss(800e-9, 60e-15, 1e-7), O("physics", O("nonlinear", false)))); + var text = string.Join("\n", res.Notes); + Assert.True(text.Contains("YVO4.index"), "an unverified vendor index is listed"); + Assert.True(!text.Contains("YVO4.n2"), "n2 is not used in a linear run"); + } + + // ---------------------------------------------------------------------------------------------- gain media, crystals, Ciddor, strict mode + + private static string Notes(UltrafastEngine.Results.RunResult r) => string.Join("\n", r.Notes) + "\n" + r.SummaryJson().GetArray("unverifiedInputs").ToJson(false); + + [Test(Category = "materials", Reference = "design: the generic defaults live in the catalogue with a status, and a run that uses one says so", Tolerance = "exact")] + public void UserGainMedium_ThermalDefaults_AreCatalogueValues_ReportedUnlessBothAreGiven() + { + var reg = new UltrafastEngine.Models.ModelRegistry(); + JsonObject Def(params object[] extra) + { + var o = O("lifetimeS", 1e-3, "dopingDensityM3", 1e26, "emission", O("kind", "gaussians", "bands", A(O("wavelengthM", 1030e-9, "sigmaM2", 2e-24, "fwhmM", 10e-9)))); + for (int i = 0; i < extra.Length; i += 2) o.Set((string)extra[i], extra[i + 1] is double d ? (JsonValue)d : (JsonValue)(string)extra[i + 1]); + return o; + } + var g = UltrafastEngine.Gain.GainMedium.FromJson(Def(), reg, "test"); + Assert.Close(10, g.ThermalConductivity, 0, 0, "default conductivity, from the catalogue"); + Assert.Close(7.8e-6, g.DnDT, 1e-12, 0, "default dn/dT, from the catalogue"); + Assert.True(g.Used.ContainsKey("thermal") && g.Used["thermal"].Path == "defaults:gainMedium.thermal" && g.Used["thermal"].Status == UltrafastEngine.Materials.CatalogueStatus.Unverified, "the run reports the default as unverified"); + var one = UltrafastEngine.Gain.GainMedium.FromJson(Def("thermalConductivity", 5.0), reg, "test"); + Assert.True(one.Used.ContainsKey("thermal"), "one value given: the other is still a default"); + var both = UltrafastEngine.Gain.GainMedium.FromJson(Def("thermalConductivity", 5.0, "dnDT", 1e-5), reg, "test"); + Assert.True(!both.Used.ContainsKey("thermal") && both.ThermalConductivity == 5.0 && both.DnDT == 1e-5, "both given: nothing is reported"); + } + + [Test(Category = "materials")] + public void StrictMaterials_AllowList_AcceptsNamedValuesOnly() + { + string Refusal(string allow) + { + try { StrictRun("FusedSilica", O("strictMaterialsAllow", A(allow))); return ""; } + catch (ModelException ex) { return ex.Message; } + } + string msg = Refusal("FusedSilica.n2"); + Assert.True(!msg.Contains("FusedSilica.n2 ") && !msg.Contains("[FusedSilica.n2]"), "the allowed value is no longer listed: " + msg); + string all = Refusal("FusedSilica"); + Assert.True(!all.Contains("[FusedSilica."), "an entry name accepts all its quantities: " + all); + Assert.True(Refusal("BaF2").Contains("FusedSilica.n2"), "an unrelated entry accepts nothing here"); + } + + [Test(Category = "materials")] + public void SolidIonization_PartialOverride_NamesWhatTheRunGaveAndWhatCameFromTheCatalogue() + { + var sys = Sys(O("id", "nl", "type", "bulk", "material", "FusedSilica", "lengthM", 1e-4, "beam", O("kind", "constant", "areaM2", 1e-8), "ionization", O("model", "keldysh", "effectiveMass", 0.5))); + var notes = string.Join("\n", Go(sys, Spec(Gauss(800e-9, 30e-15, 2e-9), O())).Notes); + Assert.True(notes.Contains("the run gives effectiveMass") && notes.Contains("catalogue values"), "the note names the overridden field: " + notes); + } + + [Test(Category = "materials")] + public void GainMedium_ListsItsCatalogueValues_AndNotTheOnesTheElementOverrides() + { + const double lam = 1030e-9; + string Run(JsonObject extra) + { + var el = O("id", "g", "type", "gain", "medium", "Yb:YAG", "lengthM", 0.003, "inversion", O("fraction", 0.25), "spm", false, "modeRadiusM", 1e-3); + if (extra != null) foreach (var kv in extra) el.Set(kv.Key, kv.Value); + return Notes(Go(Sys(el), Spec(Gauss(lam, 10e-12, 1e-9)))); + } + string t = Run(null); + Assert.True(t.Contains("Yb:YAG.emission"), "the emission band is used"); + Assert.True(t.Contains("Yb:YAG.lifetime"), "the lifetime is used"); + Assert.True(t.Contains("Yb:YAG.doping"), "the doping density is used"); + Assert.True(!t.Contains("Yb:YAG.thermal"), "no pump, no thermal lens: the thermal data are not used"); + string o = Run(O("dopingDensityM3", 1e26)); + Assert.True(!o.Contains("Yb:YAG.doping"), "an element that gives its own doping does not use the catalogue's"); + var user = UltrafastEngine.Gain.GainMedium.FromJson(O("base", "Yb:YAG", "lifetimeS", 1e-3), new UltrafastEngine.Models.ModelRegistry(), "test"); + Assert.True(!user.Used.ContainsKey("lifetime") && user.Used.ContainsKey("emission"), "a medium built from a base keeps the quantities it did not change"); + } + + [Test(Category = "materials")] + public void Crystal_ListsItsUnverifiedValues_AndVerifiedIndicesStayQuiet() + { + string Run(JsonObject extra) + { + var el = O("id", "bbo", "type", "crystal", "crystal", "BBO", "lengthM", 1e-3, "interaction", O("process", "shg", "type", "I"), + "overlap", O("model", "planeWave", "radiusM", 100e-6), "chi3", false, "output", "sh"); + if (extra != null) foreach (var kv in extra) el.Set(kv.Key, kv.Value); + return Notes(Go(Sys(el), Spec(Gauss(1030e-9, 3e-12, 1e-9), O("physics", O("selfSteepening", false))))); + } + string t = Run(null); + Assert.True(t.Contains("BBO.d"), "the d tensor of BBO is unverified and used"); + Assert.True(!t.Contains("BBO.indexX") && !t.Contains("BBO.indexZ"), "the indices are verified against the database pages"); + Assert.True(!Run(O("deffPmPerV", 2.0)).Contains("BBO.d "), "a given deff replaces the catalogue's tensor"); + Assert.True(Run(null).Contains("BBO.n2"), "n2 of BBO is listed"); + Assert.True(Catalogue.Get("LBO", "crystal").Q("indexY").Status == CatalogueStatus.Unverified, "LBO (Kato 1994) has no database page and is not claimed verified"); + } + + [Test(Category = "materials", Reference = "Ciddor, Appl. Opt. 35, 1566 (1996) as published on refractiveindex.info (other/air/Ciddor: standard air, 15 C, 101.325 kPa, dry, 450 ppm CO2): n - 1 = 0.05792105 / (238.0185 - s^2) + 0.00167917 / (57.362 - s^2)", Tolerance = "1e-12")] + public void CiddorAir_AtStandardConditions_EqualsTheDatabaseFormula() + { + var air = new CiddorAirIndex(15, 101325, 0, 450); + foreach (double um in new[] { 0.3, 0.5, 0.6328, 0.8, 1.03, 1.55, 1.69 }) + { + var n = new double[1]; air.Evaluate(new[] { 2 * Math.PI * 2.99792458e8 / (um * 1e-6) }, n); + double s2 = 1 / (um * um); + Assert.Close(1 + 0.05792105 / (238.0185 - s2) + 0.00167917 / (57.362 - s2), n[0], 1e-12, 0, "n of standard air at " + um + " um"); + } + var q = Catalogue.Get("Air", "gas").Q("standardAirDispersion"); + Assert.Close(0.05792105, q.Number("k1PerUm2") * 1e-8, 1e-12, 0, "k1 equals the page's first coefficient"); + Assert.Close(0.00167917, q.Number("k3PerUm2") * 1e-8, 1e-12, 0, "k3 equals the page's third coefficient"); + } + + private static string StrictRun(string material, JsonObject physics, JsonObject extra = null) + { + var sys = Sys(O("id", "nl", "type", "bulk", "material", material, "lengthM", 0.01, "beam", O("kind", "constant", "areaM2", 1e-8))); + var ex = extra ?? O(); + var ph = physics ?? O(); ph.Set("strictMaterials", true); ex.Set("physics", ph); + return Notes(Go(sys, Spec(Gauss(800e-9, 60e-15, 1e-7), ex))); + } + + [Test(Category = "materials")] + public void StrictMaterials_RefusesUnverifiedValues_ListsThem_AndAcceptsOverridesAndVerifiedOnes() + { + string msg = null; + try { StrictRun("FusedSilica", O()); } catch (ModelException ex) { msg = ex.Message; } + Assert.True(msg != null, "n2 of fused silica is approximate: the strict run is refused"); + Assert.True(msg.Contains("FusedSilica.n2") && msg.Contains("approximate"), "the message names the value and its status: " + msg); + Assert.True(msg.Contains("strictMaterials"), "and says how to switch it off"); + // overridden / disabled values are not counted: n2 overridden, Raman switched off, ionisation off -> only the verified index is left + StrictRun("FusedSilica", O("raman", false), O("overrides", A(O("target", "material:FusedSilica", "property", "n2", "value", 2.2e-20)))); + // a linear run on a verified index + var lin = Sys(O("id", "g", "type", "bulk", "material", "N-BK7", "lengthM", 0.01)); + Go(lin, Spec(Gauss(800e-9, 60e-15, 1e-7), O("physics", O("nonlinear", false, "strictMaterials", true)))); + Assert.Throws(() => Go(Sys(O("id", "g", "type", "bulk", "material", "YVO4", "lengthM", 0.01)), + Spec(Gauss(800e-9, 60e-15, 1e-7), O("physics", O("nonlinear", false, "strictMaterials", true)))), "the vendor Sellmeier of YVO4 is unverified"); + // the environment variable does the same without touching the run spec + Environment.SetEnvironmentVariable("ULTRAFAST_STRICT_MATERIALS", "1"); + try + { + Assert.Throws(() => Go(Sys(O("id", "g", "type", "bulk", "material", "YVO4", "lengthM", 0.01)), + Spec(Gauss(800e-9, 60e-15, 1e-7), O("physics", O("nonlinear", false)))), "ULTRAFAST_STRICT_MATERIALS=1"); + } + finally { Environment.SetEnvironmentVariable("ULTRAFAST_STRICT_MATERIALS", null); } + } + + [Test(Category = "materials")] + public void StrictMaterials_AcceptsAUserValueMarkedVerified() + { + string dir = Path.Combine(Path.GetTempPath(), "uf-cat-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + File.WriteAllText(Path.Combine(dir, "YVO4.json"), @"{ ""id"": ""YVO4"", ""quantities"": { + ""index"": { ""model"": ""vendorSellmeier"", ""referenceMedium"": ""air"", ""a"": 3.778790, ""numeratorUm2"": 0.070479, ""poleUm2"": 0.045731, ""irTermPerUm2"": 0.009701, + ""source"": ""Shi et al., J. Synthetic Cryst. 30, 85 (2001), 20 C"", ""status"": ""verified"" } } }"); + Catalogue.Reload(dir); + Go(Sys(O("id", "g", "type", "bulk", "material", "YVO4", "lengthM", 0.01)), Spec(Gauss(800e-9, 60e-15, 1e-7), O("physics", O("nonlinear", false, "strictMaterials", true)))); + } + finally { Catalogue.Reload(null); Directory.Delete(dir, true); } + } + } +} diff --git a/tests/UltrafastEngine.Tests/Data/catalogue-snapshot.txt b/tests/UltrafastEngine.Tests/Data/catalogue-snapshot.txt new file mode 100644 index 0000000..743e5a6 --- /dev/null +++ b/tests/UltrafastEngine.Tests/Data/catalogue-snapshot.txt @@ -0,0 +1,3412 @@ +ciddor:15,101325,0.5,400.n[0] 1.0002824501400973 +ciddor:15,101325,0.5,400.n[10] 1.000273565229856 +ciddor:15,101325,0.5,400.n[11] 1.0002734157872704 +ciddor:15,101325,0.5,400.n[12] 1.000273291119671 +ciddor:15,101325,0.5,400.n[13] 1.0002731860304257 +ciddor:15,101325,0.5,400.n[14] 1.0002730966191276 +ciddor:15,101325,0.5,400.n[15] 1.0002730199116245 +ciddor:15,101325,0.5,400.n[16] 1.0002729536077726 +ciddor:15,101325,0.5,400.n[17] 1.0002728959059257 +ciddor:15,101325,0.5,400.n[18] 1.0002728453785705 +ciddor:15,101325,0.5,400.n[19] 1.0002728008827426 +ciddor:15,101325,0.5,400.n[1] 1.0002795287267696 +ciddor:15,101325,0.5,400.n[20] 1.0002727614945242 +ciddor:15,101325,0.5,400.n[21] 1.0002727264604956 +ciddor:15,101325,0.5,400.n[22] 1.0002726951613004 +ciddor:15,101325,0.5,400.n[23] 1.000272667083986 +ciddor:15,101325,0.5,400.n[2] 1.0002777177173696 +ciddor:15,101325,0.5,400.n[3] 1.0002765143319468 +ciddor:15,101325,0.5,400.n[4] 1.0002756728008402 +ciddor:15,101325,0.5,400.n[5] 1.000275060698301 +ciddor:15,101325,0.5,400.n[6] 1.0002746013011221 +ciddor:15,101325,0.5,400.n[7] 1.0002742475734074 +ciddor:15,101325,0.5,400.n[8] 1.0002739693381135 +ciddor:15,101325,0.5,400.n[9] 1.000273746495431 +ciddor:15,101325,0.5,400.xw 0.008449880526810713 +ciddor:20,101325,0,450.n[0] 1.000277924845514 +ciddor:20,101325,0,450.n[10] 1.000269210549853 +ciddor:20,101325,0,450.n[11] 1.0002690640610934 +ciddor:20,101325,0,450.n[12] 1.0002689418618953 +ciddor:20,101325,0,450.n[13] 1.0002688388564902 +ciddor:20,101325,0,450.n[14] 1.0002687512204185 +ciddor:20,101325,0,450.n[15] 1.0002686760376023 +ciddor:20,101325,0,450.n[16] 1.0002686110529222 +ciddor:20,101325,0,450.n[17] 1.0002685545000765 +ciddor:20,101325,0,450.n[18] 1.0002685049796103 +ciddor:20,101325,0,450.n[19] 1.0002684613710637 +ciddor:20,101325,0,450.n[1] 1.0002750591031877 +ciddor:20,101325,0,450.n[20] 1.000268422768739 +ciddor:20,101325,0,450.n[21] 1.0002683884340942 +ciddor:20,101325,0,450.n[22] 1.000268357760018 +ciddor:20,101325,0,450.n[23] 1.0002683302437119 +ciddor:20,101325,0,450.n[2] 1.0002732826114233 +ciddor:20,101325,0,450.n[3] 1.0002721022822072 +ciddor:20,101325,0,450.n[4] 1.000271276971723 +ciddor:20,101325,0,450.n[5] 1.0002706767334097 +ciddor:20,101325,0,450.n[6] 1.0002702262835537 +ciddor:20,101325,0,450.n[7] 1.0002698794731482 +ciddor:20,101325,0,450.n[8] 1.0002696066974197 +ciddor:20,101325,0,450.n[9] 1.0002693882398754 +ciddor:20,101325,0,450.xw 0 +ciddor:25,90000,0.4,450.n[0] 1.000242263997854 +ciddor:25,90000,0.4,450.n[10] 1.0002346265813569 +ciddor:25,90000,0.4,450.n[11] 1.0002344980719142 +ciddor:25,90000,0.4,450.n[12] 1.0002343908645959 +ciddor:25,90000,0.4,450.n[13] 1.0002343004917755 +ciddor:25,90000,0.4,450.n[14] 1.0002342236000694 +ciddor:25,90000,0.4,450.n[15] 1.0002341576323477 +ciddor:25,90000,0.4,450.n[16] 1.0002341006109174 +ciddor:25,90000,0.4,450.n[17] 1.0002340509866539 +ciddor:25,90000,0.4,450.n[18] 1.000234007532095 +ciddor:25,90000,0.4,450.n[19] 1.0002339692644282 +ciddor:25,90000,0.4,450.n[1] 1.0002397530199783 +ciddor:25,90000,0.4,450.n[20] 1.000233935389178 +ciddor:25,90000,0.4,450.n[21] 1.000233905258468 +ciddor:25,90000,0.4,450.n[22] 1.0002338783396958 +ciddor:25,90000,0.4,450.n[23] 1.0002338541917524 +ciddor:25,90000,0.4,450.n[2] 1.000238196439125 +ciddor:25,90000,0.4,450.n[3] 1.0002371620467398 +ciddor:25,90000,0.4,450.n[4] 1.0002364386357254 +ciddor:25,90000,0.4,450.n[5] 1.0002359124111835 +ciddor:25,90000,0.4,450.n[6] 1.0002355174421778 +ciddor:25,90000,0.4,450.n[7] 1.0002352133065333 +ciddor:25,90000,0.4,450.n[8] 1.000234974068483 +ciddor:25,90000,0.4,450.n[9] 1.0002347824519104 +ciddor:25,90000,0.4,450.xw 0.0141412861035342 +const:BaF2N2PerW 2E-20 +const:BaF2Sigma -1.2 +const:CS2 3E-18 0.9 2E-13 1.6E-12 +crystal:AgGaSe2@20.d0 0 0 0 3.3E-11 0 0 +crystal:AgGaSe2@20.d1 0 0 0 0 3.3E-11 0 +crystal:AgGaSe2@20.d2 0 0 0 0 0 3.3E-11 +crystal:AgGaSe2@20.nx 2.7403828888726824 2.7163486685001117 2.6989163069812636 2.685808257604241 2.6756677288907103 2.6676396907359 2.661160770628905 2.6558460705704854 +crystal:AgGaSe2@20.ny 2.7403828888726824 2.7163486685001117 2.6989163069812636 2.685808257604241 2.6756677288907103 2.6676396907359 2.661160770628905 2.6558460705704854 +crystal:AgGaSe2@20.nz 2.728881951756573 2.6986456534576244 2.6773653832115136 2.6617101976460456 2.6497974061931253 2.6404864907412193 2.6330485982095087 2.6269977024832216 +crystal:AgGaSe2@20.scalars 3.5E-18 0.81 18 NaN 20 +crystal:AgGaSe2@20.text AgGaSe2|uniaxial|-42m|False|0 +crystal:AgGaSe2@80.d0 0 0 0 3.3E-11 0 0 +crystal:AgGaSe2@80.d1 0 0 0 0 3.3E-11 0 +crystal:AgGaSe2@80.d2 0 0 0 0 0 3.3E-11 +crystal:AgGaSe2@80.nx 2.7403828888726824 2.7163486685001117 2.6989163069812636 2.685808257604241 2.6756677288907103 2.6676396907359 2.661160770628905 2.6558460705704854 +crystal:AgGaSe2@80.ny 2.7403828888726824 2.7163486685001117 2.6989163069812636 2.685808257604241 2.6756677288907103 2.6676396907359 2.661160770628905 2.6558460705704854 +crystal:AgGaSe2@80.nz 2.728881951756573 2.6986456534576244 2.6773653832115136 2.6617101976460456 2.6497974061931253 2.6404864907412193 2.6330485982095087 2.6269977024832216 +crystal:AgGaSe2@80.scalars 3.5E-18 0.81 18 NaN 20 +crystal:AgGaSe2@80.text AgGaSe2|uniaxial|-42m|False|1 +crystal:BBO@20.d0 0 0 0 0 4E-14 -2.2000000000000003E-12 +crystal:BBO@20.d1 -2.2000000000000003E-12 2.2000000000000003E-12 0 4E-14 0 0 +crystal:BBO@20.d2 4E-14 4E-14 0 0 0 0 +crystal:BBO@20.nx 1.6577482810708724 1.6558748896788857 1.6542268671618205 1.6527192792522958 1.6512955085407066 1.649916377523811 1.648553837730274 1.6471871532972304 +crystal:BBO@20.ny 1.6577482810708724 1.6558748896788857 1.6542268671618205 1.6527192792522958 1.6512955085407066 1.649916377523811 1.648553837730274 1.6471871532972304 +crystal:BBO@20.nz 1.5438824696782019 1.5428499193978342 1.5420155384605159 1.541318356065698 1.5407180006598418 1.5401868537939554 1.539705486767243 1.5392598968580362 +crystal:BBO@20.scalars 2.8E-20 0.188 5.2 NaN 20 +crystal:BBO@20.text BBO|uniaxial|3m|False|0 +crystal:BBO@80.d0 0 0 0 0 4E-14 -2.2000000000000003E-12 +crystal:BBO@80.d1 -2.2000000000000003E-12 2.2000000000000003E-12 0 4E-14 0 0 +crystal:BBO@80.d2 4E-14 4E-14 0 0 0 0 +crystal:BBO@80.nx 1.6577482810708724 1.6558748896788857 1.6542268671618205 1.6527192792522958 1.6512955085407066 1.649916377523811 1.648553837730274 1.6471871532972304 +crystal:BBO@80.ny 1.6577482810708724 1.6558748896788857 1.6542268671618205 1.6527192792522958 1.6512955085407066 1.649916377523811 1.648553837730274 1.6471871532972304 +crystal:BBO@80.nz 1.5438824696782019 1.5428499193978342 1.5420155384605159 1.541318356065698 1.5407180006598418 1.5401868537939554 1.539705486767243 1.5392598968580362 +crystal:BBO@80.scalars 2.8E-20 0.188 5.2 NaN 20 +crystal:BBO@80.text BBO|uniaxial|3m|False|1 +crystal:BiBO@20.nx 1.7620173840819817 1.7593765046304826 1.7571619087600359 1.7552304501226395 1.7534885791295 1.7518729657618084 1.7503394428954873 1.748856411484557 +crystal:BiBO@20.ny 1.7895675656673886 1.7864282199463606 1.7837654375915974 1.7814146554011194 1.779268545260593 1.7772545559926798 1.7753221402211747 1.7734351554365706 +crystal:BiBO@20.nz 1.9241436997018257 1.9202172481703932 1.9169153136343153 1.9140253081977499 1.9114087690385704 1.908972198555813 1.9066505122224087 1.904397206858549 +crystal:BiBO@20.scalars 7.65E-20 0.48 3.1 NaN 20 +crystal:BiBO@20.text BiBO|biaxial||False|0 +crystal:BiBO@80.nx 1.7620173840819817 1.7593765046304826 1.7571619087600359 1.7552304501226395 1.7534885791295 1.7518729657618084 1.7503394428954873 1.748856411484557 +crystal:BiBO@80.ny 1.7895675656673886 1.7864282199463606 1.7837654375915974 1.7814146554011194 1.779268545260593 1.7772545559926798 1.7753221402211747 1.7734351554365706 +crystal:BiBO@80.nz 1.9241436997018257 1.9202172481703932 1.9169153136343153 1.9140253081977499 1.9114087690385704 1.908972198555813 1.9066505122224087 1.904397206858549 +crystal:BiBO@80.scalars 7.65E-20 0.48 3.1 NaN 20 +crystal:BiBO@80.text BiBO|biaxial||False|1 +crystal:KDP@20.d0 0 0 0 3.9E-13 0 0 +crystal:KDP@20.d1 0 0 0 0 3.9E-13 0 +crystal:KDP@20.d2 0 0 0 0 0 3.9E-13 +crystal:KDP@20.nx 1.4985144320049162 1.4960389883587693 1.4935642847169894 1.4910413403332492 1.4884372798827583 1.4857291613662436 1.482900384712876 1.4799385153403377 +crystal:KDP@20.ny 1.4985144320049162 1.4960389883587693 1.4935642847169894 1.4910413403332492 1.4884372798827583 1.4857291613662436 1.482900384712876 1.4799385153403377 +crystal:KDP@20.nz 1.4618296042421381 1.460757105128824 1.4597886526182533 1.4588819982619683 1.4580091069530063 1.4571507443858926 1.456293326614682 1.455427010881194 +crystal:KDP@20.scalars 2E-20 0.2138 1.529 NaN 20 +crystal:KDP@20.text KDP|uniaxial|-42m|False|0 +crystal:KDP@80.d0 0 0 0 3.9E-13 0 0 +crystal:KDP@80.d1 0 0 0 0 3.9E-13 0 +crystal:KDP@80.d2 0 0 0 0 0 3.9E-13 +crystal:KDP@80.nx 1.4985144320049162 1.4960389883587693 1.4935642847169894 1.4910413403332492 1.4884372798827583 1.4857291613662436 1.482900384712876 1.4799385153403377 +crystal:KDP@80.ny 1.4985144320049162 1.4960389883587693 1.4935642847169894 1.4910413403332492 1.4884372798827583 1.4857291613662436 1.482900384712876 1.4799385153403377 +crystal:KDP@80.nz 1.4618296042421381 1.460757105128824 1.4597886526182533 1.4588819982619683 1.4580091069530063 1.4571507443858926 1.456293326614682 1.455427010881194 +crystal:KDP@80.scalars 2E-20 0.2138 1.529 NaN 20 +crystal:KDP@80.text KDP|uniaxial|-42m|False|1 +crystal:KTP@20.d0 0 0 0 0 1.95E-12 0 +crystal:KTP@20.d1 0 0 0 3.9E-12 0 0 +crystal:KTP@20.d2 1.95E-12 3.9E-12 1.53E-11 0 0 0 +crystal:KTP@20.nx 1.7434106637034972 1.7402697743781337 1.7377239146934962 1.7355788026487509 1.7337074441142768 1.7320234717796181 1.7304660477719325 1.7289909076366248 +crystal:KTP@20.ny 1.7513440389162835 1.7479808258730396 1.7452505480289042 1.7429444708563604 1.7409269241848366 1.7391061233782528 1.7374177614833506 1.7358153322572016 +crystal:KTP@20.nz 1.8376040561741733 1.833053041730205 1.8293769802551976 1.8262910689826661 1.823610825045121 1.8212120542746462 1.8190083636145098 1.8169379132786065 +crystal:KTP@20.scalars 2.2E-19 0.43 3.54 NaN 20 +crystal:KTP@20.text KTP|biaxial|mm2|False|0 +crystal:KTP@80.d0 0 0 0 0 1.95E-12 0 +crystal:KTP@80.d1 0 0 0 3.9E-12 0 0 +crystal:KTP@80.d2 1.95E-12 3.9E-12 1.53E-11 0 0 0 +crystal:KTP@80.nx 1.7434106637034972 1.7402697743781337 1.7377239146934962 1.7355788026487509 1.7337074441142768 1.7320234717796181 1.7304660477719325 1.7289909076366248 +crystal:KTP@80.ny 1.7513440389162835 1.7479808258730396 1.7452505480289042 1.7429444708563604 1.7409269241848366 1.7391061233782528 1.7374177614833506 1.7358153322572016 +crystal:KTP@80.nz 1.8376040561741733 1.833053041730205 1.8293769802551976 1.8262910689826661 1.823610825045121 1.8212120542746462 1.8190083636145098 1.8169379132786065 +crystal:KTP@80.scalars 2.2E-19 0.43 3.54 NaN 20 +crystal:KTP@80.text KTP|biaxial|mm2|False|1 +crystal:LBO@20.d0 0 0 0 0 0 -6.700000000000001E-13 +crystal:LBO@20.d1 -6.700000000000001E-13 4E-14 8.499999999999999E-13 0 0 0 +crystal:LBO@20.d2 0 0 0 8.499999999999999E-13 0 0 +crystal:LBO@20.nx 1.567495923625196 1.5660233202083387 1.5646610456532088 1.5633581522865758 1.5620808413932665 1.5608059461646173 1.559517133898164 1.558202602391419 +crystal:LBO@20.ny 1.5937490856484147 1.5919654705675537 1.5902916012319044 1.5886698945328472 1.5870617627929102 1.5854402808997028 1.5837858974201335 1.5820838159886124 +crystal:LBO@20.nz 1.6086799937216552 1.606883913492503 1.6052010969105004 1.603572869012808 1.6019598847391714 1.6003346535340939 1.5986771671738513 1.5969722295174833 +crystal:LBO@20.scalars 1.9E-20 0.19 2.6 NaN 20 +crystal:LBO@20.text LBO|biaxial|mm2 (polar axis = Y)|False|1 +crystal:LBO@80.d0 0 0 0 0 0 -6.700000000000001E-13 +crystal:LBO@80.d1 -6.700000000000001E-13 4E-14 8.499999999999999E-13 0 0 0 +crystal:LBO@80.d2 0 0 0 8.499999999999999E-13 0 0 +crystal:LBO@80.nx 1.567495923625196 1.5660233202083387 1.5646610456532088 1.5633581522865758 1.5620808413932665 1.5608059461646173 1.559517133898164 1.558202602391419 +crystal:LBO@80.ny 1.5937490856484147 1.5919654705675537 1.5902916012319044 1.5886698945328472 1.5870617627929102 1.5854402808997028 1.5837858974201335 1.5820838159886124 +crystal:LBO@80.nz 1.6086799937216552 1.606883913492503 1.6052010969105004 1.603572869012808 1.6019598847391714 1.6003346535340939 1.5986771671738513 1.5969722295174833 +crystal:LBO@80.scalars 1.9E-20 0.19 2.6 NaN 20 +crystal:LBO@80.text LBO|biaxial|mm2 (polar axis = Y)|False|2 +crystal:LiNbO3@20.d0 0 0 0 0 4.6E-12 -2.1E-12 +crystal:LiNbO3@20.d1 -2.1E-12 2.1E-12 0 4.6E-12 0 0 +crystal:LiNbO3@20.d2 4.6E-12 4.6E-12 2.5E-11 0 0 0 +crystal:LiNbO3@20.nx 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:LiNbO3@20.ny 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:LiNbO3@20.nz 2.166168735480402 2.160105066767967 2.155232834611497 2.151172128724123 2.147676214795944 2.14457885251391 2.1417645193362693 2.1391507838019375 +crystal:LiNbO3@20.scalars 9.3E-20 0.4 5 NaN 20 +crystal:LiNbO3@20.text LiNbO3 (congruent)|uniaxial|3m|True|0 +crystal:LiNbO3@80.d0 0 0 0 0 4.6E-12 -2.1E-12 +crystal:LiNbO3@80.d1 -2.1E-12 2.1E-12 0 4.6E-12 0 0 +crystal:LiNbO3@80.d2 4.6E-12 4.6E-12 2.5E-11 0 0 0 +crystal:LiNbO3@80.nx 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:LiNbO3@80.ny 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:LiNbO3@80.nz 2.1688972730747422 2.1627621782904614 2.1578349003302884 2.153730681454093 2.150199600070919 2.147073248098017 2.144234585654114 2.1416000925808336 +crystal:LiNbO3@80.scalars 9.3E-20 0.4 5 NaN 80 +crystal:LiNbO3@80.text LiNbO3 (congruent)|uniaxial|3m|True|0 +crystal:MgO:LiNbO3@20.d0 0 0 0 0 4.400000000000001E-12 -2.1E-12 +crystal:MgO:LiNbO3@20.d1 -2.1E-12 2.1E-12 0 4.400000000000001E-12 0 0 +crystal:MgO:LiNbO3@20.d2 4.400000000000001E-12 4.400000000000001E-12 2.5E-11 0 0 0 +crystal:MgO:LiNbO3@20.nx 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:MgO:LiNbO3@20.ny 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:MgO:LiNbO3@20.nz 2.1572801613082127 2.1513502420399084 2.1465812235818187 2.142603738558926 2.1391775259149344 2.136140507221175 2.133379939340869 2.1308153069598776 +crystal:MgO:LiNbO3@20.scalars 9.3E-20 0.5 4 NaN 20 +crystal:MgO:LiNbO3@20.text MgO:LiNbO3 (5 mol%)|uniaxial|3m|True|1 +crystal:MgO:LiNbO3@80.d0 0 0 0 0 4.400000000000001E-12 -2.1E-12 +crystal:MgO:LiNbO3@80.d1 -2.1E-12 2.1E-12 0 4.400000000000001E-12 0 0 +crystal:MgO:LiNbO3@80.d2 4.400000000000001E-12 4.400000000000001E-12 2.5E-11 0 0 0 +crystal:MgO:LiNbO3@80.nx 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:MgO:LiNbO3@80.ny 2.244359926973829 2.237321875222457 2.2316565672160995 2.226924463866308 2.222840428437211 2.2192124772435196 2.2159072524278307 2.2128296181840446 +crystal:MgO:LiNbO3@80.nz 2.174764021473104 2.168766134211505 2.16394603793406 2.159928751026738 2.156470537099743 2.1534070662898563 2.150624080784716 2.148040013188227 +crystal:MgO:LiNbO3@80.scalars 9.3E-20 0.5 4 NaN 80 +crystal:MgO:LiNbO3@80.text MgO:LiNbO3 (5 mol%)|uniaxial|3m|True|1 +gain:Nd:YAG@293.15.s[0] 0 0 4.41432412699762E+21 +gain:Nd:YAG@293.15.s[10] 0 0 3.1946964300539473E+21 +gain:Nd:YAG@293.15.s[11] 0 0 3.108803709021196E+21 +gain:Nd:YAG@293.15.s[12] 0 0 3.027408692020642E+21 +gain:Nd:YAG@293.15.s[13] 0 0 2.950167114577617E+21 +gain:Nd:YAG@293.15.s[14] 0 0 2.876768972477097E+21 +gain:Nd:YAG@293.15.s[15] 0 0 2.8069343633626167E+21 +gain:Nd:YAG@293.15.s[16] 0 0 2.740409919660708E+21 +gain:Nd:YAG@293.15.s[17] 0 0 2.676965737000975E+21 +gain:Nd:YAG@293.15.s[18] 0 0 2.616392719649045E+21 +gain:Nd:YAG@293.15.s[19] 0 2.844284484756409E-222 2.558500278362227E+21 +gain:Nd:YAG@293.15.s[1] 0 0 4.251997169528443E+21 +gain:Nd:YAG@293.15.s[20] 0 1.0325826960201983E-66 2.503114327263593E+21 +gain:Nd:YAG@293.15.s[21] 0 7.128812198912692E-25 343686.3627515144 +gain:Nd:YAG@293.15.s[22] 0 9.359443460900135E-97 2.3992377958751383E+21 +gain:Nd:YAG@293.15.s[23] 0 2.3368099692905046E-282 2.3504668819419966E+21 +gain:Nd:YAG@293.15.s[24] 0 0 2.3036392634198104E+21 +gain:Nd:YAG@293.15.s[25] 0 0 2.25864106206438E+21 +gain:Nd:YAG@293.15.s[26] 0 0 2.2153671269318908E+21 +gain:Nd:YAG@293.15.s[27] 0 0 2.1737202140518576E+21 +gain:Nd:YAG@293.15.s[28] 0 0 2.1336102569211844E+21 +gain:Nd:YAG@293.15.s[29] 0 0 2.094953716300925E+21 +gain:Nd:YAG@293.15.s[2] 0 0 4.1011852000427854E+21 +gain:Nd:YAG@293.15.s[30] 0 0 2.057672999437137E+21 +gain:Nd:YAG@293.15.s[31] 0 0 2.0216959402089825E+21 +gain:Nd:YAG@293.15.s[32] 0 0 1.9869553328753067E+21 +gain:Nd:YAG@293.15.s[33] 0 0 1.953388513081397E+21 +gain:Nd:YAG@293.15.s[34] 0 0 1.9209369806300074E+21 +gain:Nd:YAG@293.15.s[35] 0 0 1.8895460592392248E+21 +gain:Nd:YAG@293.15.s[36] 5.053816531568984E-84 0 1.8591645891242673E+21 +gain:Nd:YAG@293.15.s[37] 0 0 1.829744648767317E+21 +gain:Nd:YAG@293.15.s[38] 0 0 1.8012413026925882E+21 +gain:Nd:YAG@293.15.s[39] 0 0 1.7736123724544006E+21 +gain:Nd:YAG@293.15.s[3] 0 0 3.960704929898171E+21 +gain:Nd:YAG@293.15.s[4] 0 0 3.8295298284136536E+21 +gain:Nd:YAG@293.15.s[5] 0 0 3.706764996593695E+21 +gain:Nd:YAG@293.15.s[6] 0 0 3.591626723635059E+21 +gain:Nd:YAG@293.15.s[7] 0 0 3.4834257387054055E+21 +gain:Nd:YAG@293.15.s[8] 0 0 3.381553401519347E+21 +gain:Nd:YAG@293.15.s[9] 0 0 3.285470247192884E+21 +gain:Nd:YAG@293.15.scalars 0.00023 1.38E+26 NaN 10.5 7.8E-06 7.5E-06 0.017 -0.0025 293.15 +gain:Nd:YAG@293.15.text Nd:YAG|YAG|0 +gain:Nd:YAG@350.s[0] 0 0 4.41432412699762E+21 +gain:Nd:YAG@350.s[10] 0 0 3.1946964300539473E+21 +gain:Nd:YAG@350.s[11] 0 0 3.108803709021196E+21 +gain:Nd:YAG@350.s[12] 0 0 3.027408692020642E+21 +gain:Nd:YAG@350.s[13] 0 0 2.950167114577617E+21 +gain:Nd:YAG@350.s[14] 0 0 2.876768972477097E+21 +gain:Nd:YAG@350.s[15] 0 0 2.8069343633626167E+21 +gain:Nd:YAG@350.s[16] 0 0 2.740409919660708E+21 +gain:Nd:YAG@350.s[17] 0 0 2.676965737000975E+21 +gain:Nd:YAG@350.s[18] 0 0 2.616392719649045E+21 +gain:Nd:YAG@350.s[19] 0 2.844284484756409E-222 2.558500278362227E+21 +gain:Nd:YAG@350.s[1] 0 0 4.251997169528443E+21 +gain:Nd:YAG@350.s[20] 0 1.0325826960201983E-66 2.503114327263593E+21 +gain:Nd:YAG@350.s[21] 0 7.128812198912692E-25 343686.3627515144 +gain:Nd:YAG@350.s[22] 0 9.359443460900135E-97 2.3992377958751383E+21 +gain:Nd:YAG@350.s[23] 0 2.3368099692905046E-282 2.3504668819419966E+21 +gain:Nd:YAG@350.s[24] 0 0 2.3036392634198104E+21 +gain:Nd:YAG@350.s[25] 0 0 2.25864106206438E+21 +gain:Nd:YAG@350.s[26] 0 0 2.2153671269318908E+21 +gain:Nd:YAG@350.s[27] 0 0 2.1737202140518576E+21 +gain:Nd:YAG@350.s[28] 0 0 2.1336102569211844E+21 +gain:Nd:YAG@350.s[29] 0 0 2.094953716300925E+21 +gain:Nd:YAG@350.s[2] 0 0 4.1011852000427854E+21 +gain:Nd:YAG@350.s[30] 0 0 2.057672999437137E+21 +gain:Nd:YAG@350.s[31] 0 0 2.0216959402089825E+21 +gain:Nd:YAG@350.s[32] 0 0 1.9869553328753067E+21 +gain:Nd:YAG@350.s[33] 0 0 1.953388513081397E+21 +gain:Nd:YAG@350.s[34] 0 0 1.9209369806300074E+21 +gain:Nd:YAG@350.s[35] 0 0 1.8895460592392248E+21 +gain:Nd:YAG@350.s[36] 5.053816531568984E-84 0 1.8591645891242673E+21 +gain:Nd:YAG@350.s[37] 0 0 1.829744648767317E+21 +gain:Nd:YAG@350.s[38] 0 0 1.8012413026925882E+21 +gain:Nd:YAG@350.s[39] 0 0 1.7736123724544006E+21 +gain:Nd:YAG@350.s[3] 0 0 3.960704929898171E+21 +gain:Nd:YAG@350.s[4] 0 0 3.8295298284136536E+21 +gain:Nd:YAG@350.s[5] 0 0 3.706764996593695E+21 +gain:Nd:YAG@350.s[6] 0 0 3.591626723635059E+21 +gain:Nd:YAG@350.s[7] 0 0 3.4834257387054055E+21 +gain:Nd:YAG@350.s[8] 0 0 3.381553401519347E+21 +gain:Nd:YAG@350.s[9] 0 0 3.285470247192884E+21 +gain:Nd:YAG@350.scalars 0.00023 1.38E+26 NaN 10.5 7.8E-06 7.5E-06 0.017 -0.0025 350 +gain:Nd:YAG@350.text Nd:YAG|YAG|0 +gain:Ti:sapphire@293.15.s[0] 1.280343634025529E-29 4.5761678122897876E-24 96463.0659862444 +gain:Ti:sapphire@293.15.s[10] 7.440927360129968E-25 1.6775405090337226E-25 350354.5196290692 +gain:Ti:sapphire@293.15.s[11] 1.493884133792839E-24 6.102416959514613E-26 199934.8580393733 +gain:Ti:sapphire@293.15.s[12] 2.7888017158388913E-24 1.9614959830849035E-26 107797.70388945851 +gain:Ti:sapphire@293.15.s[13] 4.8409347795537345E-24 5.570955001044345E-27 60872.044234744564 +gain:Ti:sapphire@293.15.s[14] 7.813609424136674E-24 1.3980688537044621E-27 36810.82807735496 +gain:Ti:sapphire@293.15.s[15] 1.1726949038329337E-23 3.1001603202203626E-28 23935.127128608277 +gain:Ti:sapphire@293.15.s[16] 1.636549891078234E-23 6.074302678401721E-29 16744.981397705338 +gain:Ti:sapphire@293.15.s[17] 2.1236568048730723E-23 1.0516358599779397E-29 12605.447383137422 +gain:Ti:sapphire@293.15.s[18] 2.562419850342585E-23 1.6087591408031975E-30 10210.631778530642 +gain:Ti:sapphire@293.15.s[19] 2.874928967633148E-23 2.174569792990345E-31 8899.351211158137 +gain:Ti:sapphire@293.15.s[1] 5.319951495683119E-29 5.7380453653160015E-24 74101.15252444614 +gain:Ti:sapphire@293.15.s[20] 2.9992648649365246E-23 2.597241639850979E-32 8345.759503800851 +gain:Ti:sapphire@293.15.s[21] 2.973733424757399E-23 2.7409937530505682E-33 8239.055709430233 +gain:Ti:sapphire@293.15.s[22] 2.887005440919032E-23 2.5559974555240785E-34 8310.472026994648 +gain:Ti:sapphire@293.15.s[23] 2.744905599110205E-23 2.1060538484790048E-35 8563.015364543415 +gain:Ti:sapphire@293.15.s[24] 2.5558859258942115E-23 1.53332849986618E-36 9013.075427510375 +gain:Ti:sapphire@293.15.s[25] 2.330718174140933E-23 9.864103655875485E-38 9690.751490779749 +gain:Ti:sapphire@293.15.s[26] 2.0814802837512657E-23 5.607079701724286E-39 10643.228976156008 +gain:Ti:sapphire@293.15.s[27] 1.8204932921695155E-23 2.816258491088731E-40 11940.281369899445 +gain:Ti:sapphire@293.15.s[28] 1.559337442914079E-23 1.2498703957288247E-41 13682.800131663022 +gain:Ti:sapphire@293.15.s[29] 1.3080530996975645E-23 4.9013321175898735E-43 16015.815541320915 +gain:Ti:sapphire@293.15.s[2] 2.055415708130974E-28 6.3574462282888786E-24 64507.86152657265 +gain:Ti:sapphire@293.15.s[30] 1.0745951694102182E-23 1.698321721764188E-44 19148.355194695992 +gain:Ti:sapphire@293.15.s[31] 8.645669633621517E-24 5.199750193078436E-46 23383.913865352384 +gain:Ti:sapphire@293.15.s[32] 6.81218810201695E-24 1.406700616441881E-47 29167.65220101614 +gain:Ti:sapphire@293.15.s[33] 5.25664767118674E-24 3.3626181942302317E-49 37160.34696006933 +gain:Ti:sapphire@293.15.s[34] 3.972513207098669E-24 7.102481675097063E-51 48355.71036484902 +gain:Ti:sapphire@293.15.s[35] 2.9400591841410812E-24 1.3255599315366325E-52 64268.98034677635 +gain:Ti:sapphire@293.15.s[36] 2.1309881705243658E-24 2.1859755327650795E-54 87244.2472858396 +gain:Ti:sapphire@293.15.s[37] 1.5126562547591685E-24 3.185282151302467E-56 120962.35631925723 +gain:Ti:sapphire@293.15.s[38] 1.0515591309930385E-24 4.101164619600543E-58 171292.44087221115 +gain:Ti:sapphire@293.15.s[39] 7.15914886159395E-25 4.6657684148725615E-60 247740.6751477318 +gain:Ti:sapphire@293.15.s[3] 7.384184547281799E-28 6.2238353692738676E-24 63630.13862421363 +gain:Ti:sapphire@293.15.s[4] 2.4666997252065865E-27 5.383815639821019E-24 71097.82939333026 +gain:Ti:sapphire@293.15.s[5] 7.661978007822545E-27 4.115086284233383E-24 89910.04933632852 +gain:Ti:sapphire@293.15.s[6] 2.2129743956268347E-26 2.7792293954523792E-24 128210.14889198508 +gain:Ti:sapphire@293.15.s[7] 5.943232900484603E-26 1.658542199268236E-24 202763.52654698348 +gain:Ti:sapphire@293.15.s[8] 1.4841574699150843E-25 8.74551401492407E-25 330563.2450202301 +gain:Ti:sapphire@293.15.s[9] 3.446260381588371E-25 4.0747487654079087E-25 436839.0176077669 +gain:Ti:sapphire@293.15.scalars 3.2E-06 3.3E+25 NaN 33 1.3E-05 NaN NaN NaN 293.15 +gain:Ti:sapphire@293.15.text Ti:sapphire|Sapphire|1 +gain:Ti:sapphire@350.s[0] 1.280343634025529E-29 4.5761678122897876E-24 96463.0659862444 +gain:Ti:sapphire@350.s[10] 7.440927360129968E-25 1.6775405090337226E-25 350354.5196290692 +gain:Ti:sapphire@350.s[11] 1.493884133792839E-24 6.102416959514613E-26 199934.8580393733 +gain:Ti:sapphire@350.s[12] 2.7888017158388913E-24 1.9614959830849035E-26 107797.70388945851 +gain:Ti:sapphire@350.s[13] 4.8409347795537345E-24 5.570955001044345E-27 60872.044234744564 +gain:Ti:sapphire@350.s[14] 7.813609424136674E-24 1.3980688537044621E-27 36810.82807735496 +gain:Ti:sapphire@350.s[15] 1.1726949038329337E-23 3.1001603202203626E-28 23935.127128608277 +gain:Ti:sapphire@350.s[16] 1.636549891078234E-23 6.074302678401721E-29 16744.981397705338 +gain:Ti:sapphire@350.s[17] 2.1236568048730723E-23 1.0516358599779397E-29 12605.447383137422 +gain:Ti:sapphire@350.s[18] 2.562419850342585E-23 1.6087591408031975E-30 10210.631778530642 +gain:Ti:sapphire@350.s[19] 2.874928967633148E-23 2.174569792990345E-31 8899.351211158137 +gain:Ti:sapphire@350.s[1] 5.319951495683119E-29 5.7380453653160015E-24 74101.15252444614 +gain:Ti:sapphire@350.s[20] 2.9992648649365246E-23 2.597241639850979E-32 8345.759503800851 +gain:Ti:sapphire@350.s[21] 2.973733424757399E-23 2.7409937530505682E-33 8239.055709430233 +gain:Ti:sapphire@350.s[22] 2.887005440919032E-23 2.5559974555240785E-34 8310.472026994648 +gain:Ti:sapphire@350.s[23] 2.744905599110205E-23 2.1060538484790048E-35 8563.015364543415 +gain:Ti:sapphire@350.s[24] 2.5558859258942115E-23 1.53332849986618E-36 9013.075427510375 +gain:Ti:sapphire@350.s[25] 2.330718174140933E-23 9.864103655875485E-38 9690.751490779749 +gain:Ti:sapphire@350.s[26] 2.0814802837512657E-23 5.607079701724286E-39 10643.228976156008 +gain:Ti:sapphire@350.s[27] 1.8204932921695155E-23 2.816258491088731E-40 11940.281369899445 +gain:Ti:sapphire@350.s[28] 1.559337442914079E-23 1.2498703957288247E-41 13682.800131663022 +gain:Ti:sapphire@350.s[29] 1.3080530996975645E-23 4.9013321175898735E-43 16015.815541320915 +gain:Ti:sapphire@350.s[2] 2.055415708130974E-28 6.3574462282888786E-24 64507.86152657265 +gain:Ti:sapphire@350.s[30] 1.0745951694102182E-23 1.698321721764188E-44 19148.355194695992 +gain:Ti:sapphire@350.s[31] 8.645669633621517E-24 5.199750193078436E-46 23383.913865352384 +gain:Ti:sapphire@350.s[32] 6.81218810201695E-24 1.406700616441881E-47 29167.65220101614 +gain:Ti:sapphire@350.s[33] 5.25664767118674E-24 3.3626181942302317E-49 37160.34696006933 +gain:Ti:sapphire@350.s[34] 3.972513207098669E-24 7.102481675097063E-51 48355.71036484902 +gain:Ti:sapphire@350.s[35] 2.9400591841410812E-24 1.3255599315366325E-52 64268.98034677635 +gain:Ti:sapphire@350.s[36] 2.1309881705243658E-24 2.1859755327650795E-54 87244.2472858396 +gain:Ti:sapphire@350.s[37] 1.5126562547591685E-24 3.185282151302467E-56 120962.35631925723 +gain:Ti:sapphire@350.s[38] 1.0515591309930385E-24 4.101164619600543E-58 171292.44087221115 +gain:Ti:sapphire@350.s[39] 7.15914886159395E-25 4.6657684148725615E-60 247740.6751477318 +gain:Ti:sapphire@350.s[3] 7.384184547281799E-28 6.2238353692738676E-24 63630.13862421363 +gain:Ti:sapphire@350.s[4] 2.4666997252065865E-27 5.383815639821019E-24 71097.82939333026 +gain:Ti:sapphire@350.s[5] 7.661978007822545E-27 4.115086284233383E-24 89910.04933632852 +gain:Ti:sapphire@350.s[6] 2.2129743956268347E-26 2.7792293954523792E-24 128210.14889198508 +gain:Ti:sapphire@350.s[7] 5.943232900484603E-26 1.658542199268236E-24 202763.52654698348 +gain:Ti:sapphire@350.s[8] 1.4841574699150843E-25 8.74551401492407E-25 330563.2450202301 +gain:Ti:sapphire@350.s[9] 3.446260381588371E-25 4.0747487654079087E-25 436839.0176077669 +gain:Ti:sapphire@350.scalars 3.2E-06 3.3E+25 NaN 33 1.3E-05 NaN NaN NaN 350 +gain:Ti:sapphire@350.text Ti:sapphire|Sapphire|1 +gain:Yb:YAG@293.15.s[0] 0 0 4.41432412699762E+21 +gain:Yb:YAG@293.15.s[10] 4.889552236757575E-232 9.283018550863856E-220 3.1946964300539473E+21 +gain:Yb:YAG@293.15.s[11] 2.342885014083241E-214 5.327432994117959E-203 3.108803709021196E+21 +gain:Yb:YAG@293.15.s[12] 1.821816654407653E-197 5.544727598485764E-187 3.027408692020642E+21 +gain:Yb:YAG@293.15.s[13] 2.298953412540306E-181 1.0377239024831336E-171 2.950167114577617E+21 +gain:Yb:YAG@293.15.s[14] 4.707902741015701E-166 3.465743194678974E-157 2.876768972477097E+21 +gain:Yb:YAG@293.15.s[15] 1.564575781043192E-151 2.0512588159947726E-143 2.8069343633626167E+21 +gain:Yb:YAG@293.15.s[16] 8.437961381527662E-138 2.1381361638369767E-130 2.740409919660708E+21 +gain:Yb:YAG@293.15.s[17] 7.384996140518004E-125 3.9027805862068623E-118 2.676965737000975E+21 +gain:Yb:YAG@293.15.s[18] 1.0489017769172577E-112 1.2410610194754477E-106 2.616392719649045E+21 +gain:Yb:YAG@293.15.s[19] 2.4176374742432507E-101 6.8430116603979436E-96 2.558500278362227E+21 +gain:Yb:YAG@293.15.s[1] 0 0 4.251997169528443E+21 +gain:Yb:YAG@293.15.s[20] 9.043141899139044E-91 6.514312379579629E-86 2.503114327263593E+21 +gain:Yb:YAG@293.15.s[21] 5.489330699115582E-81 1.0664670541127723E-76 2.450075535382929E+21 +gain:Yb:YAG@293.15.s[22] 1.266224877994596E-69 7.005443200896697E-66 2.3992377958751383E+21 +gain:Yb:YAG@293.15.s[23] 1.1252543713807015E-54 1.865734446351016E-51 2.3504668819419966E+21 +gain:Yb:YAG@293.15.s[24] 6.6387252979686735E-43 3.4610691306197187E-40 6.64311804550842E+20 +gain:Yb:YAG@293.15.s[25] 2.5964254940771755E-34 4.453054971555212E-32 5042713477770.343 +gain:Yb:YAG@293.15.s[26] 6.74929315329016E-29 3.9737355564325455E-27 54819150.50161431 +gain:Yb:YAG@293.15.s[27] 1.1772253904701925E-26 2.4769496405852257E-25 837762.94790348 +gain:Yb:YAG@293.15.s[28] 4.708419534305411E-26 3.67740586083926E-25 514340.1147784983 +gain:Yb:YAG@293.15.s[29] 1.5416334335536044E-25 4.63288963721806E-25 339289.9649558046 +gain:Yb:YAG@293.15.s[2] 0 0 4.1011852000427854E+21 +gain:Yb:YAG@293.15.s[30] 1.487570936423873E-26 1.7795734557428676E-26 6298077.93120679 +gain:Yb:YAG@293.15.s[31] 1.369246925884357E-26 6.734045135446436E-27 9897410.30302323 +gain:Yb:YAG@293.15.s[32] 1.70867105419739E-25 3.561854723080607E-26 962272.8297922065 +gain:Yb:YAG@293.15.s[33] 3.4900803985216968E-25 3.174473702356555E-26 513033.2939683107 +gain:Yb:YAG@293.15.s[34] 1.2484328282624925E-24 5.093160851304295E-26 147836.6596977941 +gain:Yb:YAG@293.15.s[35] 3.0033482279673825E-25 5.641498379136953E-27 617546.4990158874 +gain:Yb:YAG@293.15.s[36] 4.3275535584245344E-27 3.8373330256933814E-29 42583502.57637375 +gain:Yb:YAG@293.15.s[37] 1.7766919970219326E-31 7.6157895346390206E-34 1025464699842.1898 +gain:Yb:YAG@293.15.s[38] 1.6811828812426754E-35 3.563434737196839E-38 10691469009949788 +gain:Yb:YAG@293.15.s[39] 6.217248940779364E-40 6.658645563028872E-43 2.8496765863721638E+20 +gain:Yb:YAG@293.15.s[3] 0 0 3.960704929898171E+21 +gain:Yb:YAG@293.15.s[4] 0 0 3.8295298284136536E+21 +gain:Yb:YAG@293.15.s[5] 0 0 3.706764996593695E+21 +gain:Yb:YAG@293.15.s[6] 1.1750589231817E-310 4.051853200797846E-294 3.591626723635059E+21 +gain:Yb:YAG@293.15.s[7] 8.118098049883395E-290 1.9320630018179757E-274 3.4834257387054055E+21 +gain:Yb:YAG@293.15.s[8] 9.101662957437944E-270 1.748110369192295E-255 3.381553401519347E+21 +gain:Yb:YAG@293.15.s[9] 1.6559949768483938E-250 2.961473063270817E-237 3.285470247192884E+21 +gain:Yb:YAG@293.15.scalars 0.00095 2.76E+26 9.688E-07 9.5 7.8E-06 7.5E-06 0.017 -0.0025 293.15 +gain:Yb:YAG@293.15.text Yb:YAG|YAG|1 +gain:Yb:YAG@350.s[0] 0 0 4.41432412699762E+21 +gain:Yb:YAG@350.s[10] 4.889552236757575E-232 9.404224269568907E-222 3.1946964300539473E+21 +gain:Yb:YAG@350.s[11] 2.342885014083241E-214 7.618238288684386E-205 3.108803709021196E+21 +gain:Yb:YAG@350.s[12] 1.821816654407653E-197 1.0992095707872884E-188 3.027408692020642E+21 +gain:Yb:YAG@350.s[13] 2.298953412540306E-181 2.804831576133586E-173 2.950167114577617E+21 +gain:Yb:YAG@350.s[14] 4.707902741015701E-166 1.2576137826722331E-158 2.876768972477097E+21 +gain:Yb:YAG@350.s[15] 1.564575781043192E-151 9.851138252115713E-145 2.8069343633626167E+21 +gain:Yb:YAG@350.s[16] 8.437961381527662E-138 1.341056437176133E-131 2.740409919660708E+21 +gain:Yb:YAG@350.s[17] 7.384996140518004E-125 3.157642612112424E-119 2.676965737000975E+21 +gain:Yb:YAG@350.s[18] 1.0489017769172577E-112 1.2804273700545028E-107 2.616392719649045E+21 +gain:Yb:YAG@350.s[19] 2.4176374742432507E-101 8.906561721562353E-97 2.558500278362227E+21 +gain:Yb:YAG@350.s[1] 0 0 4.251997169528443E+21 +gain:Yb:YAG@350.s[20] 9.043141899139044E-91 1.0589215764293083E-86 2.503114327263593E+21 +gain:Yb:YAG@350.s[21] 5.48933439794576E-81 2.144788609806801E-77 2.450075535382929E+21 +gain:Yb:YAG@350.s[22] 5.117634476077552E-69 6.982904677715783E-66 2.3992377958751383E+21 +gain:Yb:YAG@350.s[23] 3.7515055590454873E-54 1.865734445347638E-51 2.3504668819419966E+21 +gain:Yb:YAG@350.s[24] 1.8341021673483293E-42 3.461069130619644E-40 6.620775253821635E+20 +gain:Yb:YAG@350.s[25] 5.988073595476788E-34 4.453054971555211E-32 5004815509128.068 +gain:Yb:YAG@350.s[26] 1.308420158560583E-28 3.973735556432544E-27 53973084.63332711 +gain:Yb:YAG@350.s[27] 1.9309097222116505E-26 2.4769496405846385E-25 814115.0376613974 +gain:Yb:YAG@350.s[28] 6.57460525321557E-26 3.6774058579863937E-25 492197.4677551697 +gain:Yb:YAG@350.s[29] 1.8433203771007806E-25 4.632887789091663E-25 323484.61669857206 +gain:Yb:YAG@350.s[2] 0 0 4.1011852000427854E+21 +gain:Yb:YAG@350.s[30] 1.5309875630799215E-26 1.778962297601209E-26 6216628.909942774 +gain:Yb:YAG@350.s[31] 1.3692423986401372E-26 7.556766569423745E-27 9514225.659079365 +gain:Yb:YAG@350.s[32] 1.7086710541966392E-25 4.595017584271993E-26 916419.2638642906 +gain:Yb:YAG@350.s[33] 3.4900803985216968E-25 4.685829322901951E-26 493446.4868172489 +gain:Yb:YAG@350.s[34] 1.2484328282624925E-24 8.563711764423213E-26 143990.7245136542 +gain:Yb:YAG@350.s[35] 3.0033482279673825E-25 1.0759212633700597E-26 607387.4276068306 +gain:Yb:YAG@350.s[36] 4.3275535584245344E-27 8.267372592270374E-29 42155754.55085517 +gain:Yb:YAG@350.s[37] 1.7766919970219326E-31 1.846414045609271E-33 1019267689627.2009 +gain:Yb:YAG@350.s[38] 1.6811828812426754E-35 9.68638165495065E-38 10652753244835846 +gain:Yb:YAG@350.s[39] 6.217248940779364E-40 2.0222425701919167E-42 2.8434797825051027E+20 +gain:Yb:YAG@350.s[3] 0 0 3.960704929898171E+21 +gain:Yb:YAG@350.s[4] 0 0 3.8295298284136536E+21 +gain:Yb:YAG@350.s[5] 0 0 3.706764996593695E+21 +gain:Yb:YAG@350.s[6] 1.1750589231817E-310 8.34595280998957E-297 3.591626723635059E+21 +gain:Yb:YAG@350.s[7] 8.118098049883395E-290 6.14366425762717E-277 3.4834257387054055E+21 +gain:Yb:YAG@350.s[8] 9.101662957437944E-270 8.366210335069866E-258 3.381553401519347E+21 +gain:Yb:YAG@350.s[9] 1.6559949768483938E-250 2.08416195973427E-239 3.285470247192884E+21 +gain:Yb:YAG@350.scalars 0.00095 2.76E+26 9.688E-07 9.5 7.8E-06 7.5E-06 0.017 -0.0025 350 +gain:Yb:YAG@350.text Yb:YAG|YAG|1 +gas:Air@1bar293K.fR 0 +gas:Air@1bar293K.indexReference vacuum +gas:Air@1bar293K.isGas True +gas:Air@1bar293K.n2 7.915742793791574E-24 +gas:Air@1bar293K.n[0] 1.000274289223731 +gas:Air@1bar293K.n[10] 1.0002656889224957 +gas:Air@1bar293K.n[11] 1.0002655443500015 +gas:Air@1bar293K.n[12] 1.0002654237493294 +gas:Air@1bar293K.n[13] 1.0002653220913702 +gas:Air@1bar293K.n[14] 1.0002652356016932 +gas:Air@1bar293K.n[15] 1.000265161402367 +gas:Air@1bar293K.n[16] 1.0002650972677718 +gas:Air@1bar293K.n[17] 1.0002650414547114 +gas:Air@1bar293K.n[18] 1.000264992582038 +gas:Air@1bar293K.n[19] 1.0002649495439484 +gas:Air@1bar293K.n[1] 1.000271460969076 +gas:Air@1bar293K.n[20] 1.000264911446593 +gas:Air@1bar293K.n[21] 1.00026487756109 +gas:Air@1bar293K.n[22] 1.0002648472882711 +gas:Air@1bar293K.n[23] 1.0002648201319144 +gas:Air@1bar293K.n[2] 1.0002697077161558 +gas:Air@1bar293K.n[3] 1.0002685428271951 +gas:Air@1bar293K.n[4] 1.0002677283128554 +gas:Air@1bar293K.n[5] 1.000267135926447 +gas:Air@1bar293K.n[6] 1.0002666913690665 +gas:Air@1bar293K.n[7] 1.0002663490953962 +gas:Air@1bar293K.n[8] 1.0002660798879324 +gas:Air@1bar293K.n[9] 1.0002658642880995 +gas:Air@1bar293K.rot[0] 2248554931241.2383 1.5487448307931716E-12 10000000000 +gas:Air@1bar293K.rot[10] 17225944739207.072 2.895304899338723E-11 10000000000 +gas:Air@1bar293K.rot[11] 18721249017470.758 1.3555642710658198E-11 10000000000 +gas:Air@1bar293K.rot[12] 20215902305752.38 2.4580604194305864E-11 10000000000 +gas:Air@1bar293K.rot[13] 21709852524853.36 1.0809366978115291E-11 10000000000 +gas:Air@1bar293K.rot[14] 23203047595575.168 1.8471835209758784E-11 10000000000 +gas:Air@1bar293K.rot[15] 24695435438719.223 7.675873013337157E-12 10000000000 +gas:Air@1bar293K.rot[16] 26186963975086.938 1.2422444322612101E-11 10000000000 +gas:Air@1bar293K.rot[17] 27677581125479.77 4.897674689897792E-12 10000000000 +gas:Air@1bar293K.rot[18] 29167234810699.152 7.531857071098193E-12 10000000000 +gas:Air@1bar293K.rot[19] 30655872951546.543 2.825427552131922E-12 10000000000 +gas:Air@1bar293K.rot[1] 3747504753404.4556 2.2346473107730545E-12 10000000000 +gas:Air@1bar293K.rot[20] 32143443468823.34 4.1388587747328054E-12 10000000000 +gas:Air@1bar293K.rot[21] 33629894283330.984 1.480355810319477E-12 10000000000 +gas:Air@1bar293K.rot[22] 35115173315870.902 2.069335854148143E-12 10000000000 +gas:Air@1bar293K.rot[23] 36599228487244.516 7.068141812680453E-13 10000000000 +gas:Air@1bar293K.rot[24] 38082007718253.41 9.44152052581061E-13 10000000000 +gas:Air@1bar293K.rot[25] 39563458929698.9 3.0834702868167457E-13 10000000000 +gas:Air@1bar293K.rot[26] 41043530042382.28 3.9402635128931043E-13 10000000000 +gas:Air@1bar293K.rot[27] 42522168977105.15 1.231614049770568E-13 10000000000 +gas:Air@1bar293K.rot[28] 43999323654669.02 1.5069371162349673E-13 10000000000 +gas:Air@1bar293K.rot[29] 45474941995875.24 4.511763992152179E-14 10000000000 +gas:Air@1bar293K.rot[2] 5246324377571.26 8.432890612758489E-12 10000000000 +gas:Air@1bar293K.rot[30] 46948971921525.13 5.28958144409244E-14 10000000000 +gas:Air@1bar293K.rot[31] 48421361352420.23 1.517983786097864E-14 10000000000 +gas:Air@1bar293K.rot[32] 49892058209362.02 1.7063415694220902E-14 10000000000 +gas:Air@1bar293K.rot[33] 51361010413151.92 4.696297154366833E-15 10000000000 +gas:Air@1bar293K.rot[34] 52828165884591.31 5.064187319058339E-15 10000000000 +gas:Air@1bar293K.rot[35] 54293472544481.67 1.33739288144353E-15 10000000000 +gas:Air@1bar293K.rot[36] 55756878313624.31 1.3841103558852974E-15 10000000000 +gas:Air@1bar293K.rot[37] 57218331112820.71 3.5088893745475307E-16 10000000000 +gas:Air@1bar293K.rot[38] 58677778862872.484 3.4867296045724785E-16 10000000000 +gas:Air@1bar293K.rot[39] 60135169484580.92 8.488614728502076E-17 10000000000 +gas:Air@1bar293K.rot[3] 6744961724543.089 6.502392314685109E-12 10000000000 +gas:Air@1bar293K.rot[40] 61590450898747.58 8.101841932395434E-17 10000000000 +gas:Air@1bar293K.rot[41] 63043571026173.81 1.8948546603840112E-17 10000000000 +gas:Air@1bar293K.rot[42] 64494477787660.836 1.7376749200437822E-17 10000000000 +gas:Air@1bar293K.rot[43] 65943119104010.45 3.905491223531443E-18 10000000000 +gas:Air@1bar293K.rot[44] 67389442896023.88 3.4423110478093625E-18 10000000000 +gas:Air@1bar293K.rot[45] 68833397084502.48 7.437100711628889E-19 10000000000 +gas:Air@1bar293K.rot[46] 70274929590247.914 6.302127019217455E-19 10000000000 +gas:Air@1bar293K.rot[47] 71713988334061.53 1.3092139203905096E-19 10000000000 +gas:Air@1bar293K.rot[48] 73150521236744.78 1.066899009766031E-19 10000000000 +gas:Air@1bar293K.rot[49] 74584476219099.25 2.1317382955996322E-20 10000000000 +gas:Air@1bar293K.rot[4] 8243364715121.375 1.7702880843632866E-11 10000000000 +gas:Air@1bar293K.rot[50] 76015801201926.02 1.671053544482803E-20 10000000000 +gas:Air@1bar293K.rot[51] 77444444106026.45 3.212183120141996E-21 10000000000 +gas:Air@1bar293K.rot[52] 78870352852202.34 2.4227619019291734E-21 10000000000 +gas:Air@1bar293K.rot[53] 80293475361255.27 4.481551414975817E-22 10000000000 +gas:Air@1bar293K.rot[54] 81713759553986.44 3.2531160255188855E-22 10000000000 +gas:Air@1bar293K.rot[55] 2707960287006.391 2.5396698459211867E-12 12500000000 +gas:Air@1bar293K.rot[56] 4873868076822.109 7.672188408311773E-12 12500000000 +gas:Air@1bar293K.rot[57] 7038986541284.577 1.3803485289948688E-11 12500000000 +gas:Air@1bar293K.rot[58] 9202964869125.686 1.9165480726298956E-11 12500000000 +gas:Air@1bar293K.rot[59] 11365452249077.332 2.242020958106825E-11 12500000000 +gas:Air@1bar293K.rot[5] 9741481270107.557 1.1029719712793357E-11 10000000000 +gas:Air@1bar293K.rot[60] 13526097869871.385 2.3017686134891335E-11 12500000000 +gas:Air@1bar293K.rot[61] 15684550920239.756 2.1207961829071023E-11 12500000000 +gas:Air@1bar293K.rot[62] 17840460588914.344 1.7777456875084953E-11 12500000000 +gas:Air@1bar293K.rot[63] 19993476064626.973 1.367906249830738E-11 12500000000 +gas:Air@1bar293K.rot[64] 22143246536109.605 9.721900500836788E-12 12500000000 +gas:Air@1bar293K.rot[65] 24289421192094.09 6.4106758864925756E-12 12500000000 +gas:Air@1bar293K.rot[66] 26431649221312.383 3.935294335846823E-12 12500000000 +gas:Air@1bar293K.rot[67] 28569579812496.28 2.2547704655312623E-12 12500000000 +gas:Air@1bar293K.rot[68] 30702862154377.707 1.2083042721996552E-12 12500000000 +gas:Air@1bar293K.rot[69] 32831145435688.547 6.066345922175111E-13 12500000000 +gas:Air@1bar293K.rot[6] 11239259310303.064 2.5676175574338073E-11 10000000000 +gas:Air@1bar293K.rot[70] 34954078845160.746 2.8573137056584236E-13 12500000000 +gas:Air@1bar293K.rot[71] 37071311571526.16 1.2640883420335603E-13 12500000000 +gas:Air@1bar293K.rot[72] 39182492803516.59 5.258023652542992E-14 12500000000 +gas:Air@1bar293K.rot[73] 41287271729863.99 2.0581372871146562E-14 12500000000 +gas:Air@1bar293K.rot[74] 43385297539300.414 7.58700325236395E-15 12500000000 +gas:Air@1bar293K.rot[75] 45476219420557.46 2.635804993315924E-15 12500000000 +gas:Air@1bar293K.rot[76] 47559686562367.14 8.635329365978664E-16 12500000000 +gas:Air@1bar293K.rot[77] 49635348153461.5 2.669444024330485E-16 12500000000 +gas:Air@1bar293K.rot[78] 51702853382572.15 7.79067573853174E-17 12500000000 +gas:Air@1bar293K.rot[79] 53761851438431.26 2.147651883723735E-17 12500000000 +gas:Air@1bar293K.rot[7] 12736646756509.334 1.4132484773105803E-11 10000000000 +gas:Air@1bar293K.rot[80] 55811991509770.43 5.59497144688025E-18 12500000000 +gas:Air@1bar293K.rot[81] 57852922785321.84 1.378094104782536E-18 12500000000 +gas:Air@1bar293K.rot[82] 59884294453817.09 3.210713409947919E-19 12500000000 +gas:Air@1bar293K.rot[83] 61905755703988.36 7.078723334880178E-20 12500000000 +gas:Air@1bar293K.rot[84] 63916955724567.35 1.4774875144405647E-20 12500000000 +gas:Air@1bar293K.rot[85] 65917543704285.9 2.920718516599739E-21 12500000000 +gas:Air@1bar293K.rot[86] 67907168831875.95 5.4705599455862795E-22 12500000000 +gas:Air@1bar293K.rot[8] 14233591529527.81 2.966955730430689E-11 10000000000 +gas:Air@1bar293K.rot[9] 15730041550159.912 1.4933620153602686E-11 10000000000 +gas:Air@1bar293K.sigma 0 +gas:Air@2.5bar310K.fR 0 +gas:Air@2.5bar310K.indexReference vacuum +gas:Air@2.5bar310K.isGas True +gas:Air@2.5bar310K.n2 1.8713709677419355E-23 +gas:Air@2.5bar310K.n[0] 1.0006485547204098 +gas:Air@2.5bar310K.n[10] 1.0006282193755236 +gas:Air@2.5bar310K.n[11] 1.0006278775349935 +gas:Air@2.5bar310K.n[12] 1.0006275923756491 +gas:Air@2.5bar310K.n[13] 1.0006273520062012 +gas:Air@2.5bar310K.n[14] 1.000627147502038 +gas:Air@2.5bar310K.n[15] 1.0006269720583123 +gas:Air@2.5bar310K.n[16] 1.0006268204125646 +gas:Air@2.5bar310K.n[17] 1.0006266884430204 +gas:Air@2.5bar310K.n[18] 1.000626572883967 +gas:Air@2.5bar310K.n[19] 1.0006264711207418 +gas:Air@2.5bar310K.n[1] 1.0006418673344377 +gas:Air@2.5bar310K.n[20] 1.0006263810398428 +gas:Air@2.5bar310K.n[21] 1.000626300917837 +gas:Air@2.5bar310K.n[22] 1.000626229337994 +gas:Air@2.5bar310K.n[23] 1.0006261651270012 +gas:Air@2.5bar310K.n[2] 1.0006377217816447 +gas:Air@2.5bar310K.n[3] 1.0006349674108246 +gas:Air@2.5bar310K.n[4] 1.00063304149805 +gas:Air@2.5bar310K.n[5] 1.000631640805029 +gas:Air@2.5bar310K.n[6] 1.0006305896525856 +gas:Air@2.5bar310K.n[7] 1.0006297803491744 +gas:Air@2.5bar310K.n[8] 1.0006291438102353 +gas:Air@2.5bar310K.n[9] 1.0006286340261197 +gas:Air@2.5bar310K.rot[0] 2248554931241.2383 3.279831044765914E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[10] 17225944739207.072 6.563598728943963E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[11] 18721249017470.758 3.1119022755295505E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[12] 20215902305752.38 5.72019216322835E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[13] 21709852524853.36 2.5526101186239837E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[14] 23203047595575.168 4.431126739247952E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[15] 24695435438719.223 1.8724289442703243E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[16] 26186963975086.938 3.084688967705375E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[17] 27677581125479.77 1.2392932943648526E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[18] 29167234810699.152 1.9441014220464006E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[19] 30655872951546.543 7.447084599362583E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[1] 3747504753404.4556 4.742320593337793E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[20] 32143443468823.34 1.115116621041551E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[21] 33629894283330.984 4.081270514008145E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[22] 35115173315870.902 5.8438835674551414E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[23] 36599228487244.516 2.0467675753192267E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[24] 38082007718253.41 2.8063962115095418E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[25] 39563458929698.9 9.417623072352349E-13 24311072082.62218 +gas:Air@2.5bar310K.rot[26] 41043530042382.28 1.237860371846568E-12 24311072082.62218 +gas:Air@2.5bar310K.rot[27] 42522168977105.15 3.983968289406857E-13 24311072082.62218 +gas:Air@2.5bar310K.rot[28] 43999323654669.02 5.024360757502346E-13 24311072082.62218 +gas:Air@2.5bar310K.rot[29] 45474941995875.24 1.5521214603862567E-13 24311072082.62218 +gas:Air@2.5bar310K.rot[2] 5246324377571.26 1.7952454375958194E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[30] 46948971921525.13 1.8795031739195367E-13 24311072082.62218 +gas:Air@2.5bar310K.rot[31] 48421361352420.23 5.576730550512536E-14 24311072082.62218 +gas:Air@2.5bar310K.rot[32] 49892058209362.02 6.488094611791701E-14 24311072082.62218 +gas:Air@2.5bar310K.rot[33] 51361010413151.92 1.8500901297479456E-14 24311072082.62218 +gas:Air@2.5bar310K.rot[34] 52828165884591.31 2.0690936886099122E-14 24311072082.62218 +gas:Air@2.5bar310K.rot[35] 54293472544481.67 5.67294992776019E-15 24311072082.62218 +gas:Air@2.5bar310K.rot[36] 55756878313624.31 6.101634295142296E-15 24311072082.62218 +gas:Air@2.5bar310K.rot[37] 57218331112820.71 1.6092224745070981E-15 24311072082.62218 +gas:Air@2.5bar310K.rot[38] 58677778862872.484 1.6652542297845797E-15 24311072082.62218 +gas:Air@2.5bar310K.rot[39] 60135169484580.92 4.2262902228630616E-16 24311072082.62218 +gas:Air@2.5bar310K.rot[3] 6744961724543.089 1.3900840561814032E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[40] 61590450898747.58 4.20930374760739E-16 24311072082.62218 +gas:Air@2.5bar310K.rot[41] 63043571026173.81 1.0283705137757589E-16 24311072082.62218 +gas:Air@2.5bar310K.rot[42] 64494477787660.836 9.861252145428727E-17 24311072082.62218 +gas:Air@2.5bar310K.rot[43] 65943119104010.45 2.3199093248371125E-17 24311072082.62218 +gas:Air@2.5bar310K.rot[44] 67389442896023.88 2.1424891681924758E-17 24311072082.62218 +gas:Air@2.5bar310K.rot[45] 68833397084502.48 4.854964837636933E-18 24311072082.62218 +gas:Air@2.5bar310K.rot[46] 70274929590247.914 4.319401180973207E-18 24311072082.62218 +gas:Air@2.5bar310K.rot[47] 71713988334061.53 9.430622405655821E-19 24311072082.62218 +gas:Air@2.5bar310K.rot[48] 73150521236744.78 8.085094525031859E-19 24311072082.62218 +gas:Air@2.5bar310K.rot[49] 74584476219099.25 1.7012405032782442E-19 24311072082.62218 +gas:Air@2.5bar310K.rot[4] 8243364715121.375 3.804410741336887E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[50] 76015801201926.02 1.405819284858582E-19 24311072082.62218 +gas:Air@2.5bar310K.rot[51] 77444444106026.45 2.851564889622538E-20 24311072082.62218 +gas:Air@2.5bar310K.rot[52] 78870352852202.34 2.2718167940445174E-20 24311072082.62218 +gas:Air@2.5bar310K.rot[53] 80293475361255.27 4.443299256796363E-21 24311072082.62218 +gas:Air@2.5bar310K.rot[54] 81713759553986.44 3.413701653533262E-21 24311072082.62218 +gas:Air@2.5bar310K.rot[55] 83131153351196.95 6.439298343060668E-22 24311072082.62218 +gas:Air@2.5bar310K.rot[56] 2707960287006.391 5.38381148829787E-12 30388840103.27773 +gas:Air@2.5bar310K.rot[57] 4873868076822.109 1.63509201050051E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[58] 7038986541284.577 2.966482820899545E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[59] 9202964869125.686 4.166027950039856E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[5] 9741481270107.557 2.3852749754584813E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[60] 11365452249077.332 4.944359003124041E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[61] 13526097869871.385 5.165559769810309E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[62] 15684550920239.756 4.857987986279708E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[63] 17840460588914.344 4.169115581192745E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[64] 19993476064626.973 3.2942829472066027E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[65] 22143246536109.605 2.4115579615140314E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[66] 24289421192094.09 1.6428657101608442E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[67] 26431649221312.383 1.0450444364148792E-11 30388840103.27773 +gas:Air@2.5bar310K.rot[68] 28569579812496.28 6.22335219221841E-12 30388840103.27773 +gas:Air@2.5bar310K.rot[69] 30702862154377.707 3.476686245760955E-12 30388840103.27773 +gas:Air@2.5bar310K.rot[6] 11239259310303.064 5.5935782362528516E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[70] 32831145435688.547 1.825086168765377E-12 30388840103.27773 +gas:Air@2.5bar310K.rot[71] 34954078845160.746 9.015255275826751E-13 30388840103.27773 +gas:Air@2.5bar310K.rot[72] 37071311571526.16 4.1952238255805283E-13 30388840103.27773 +gas:Air@2.5bar310K.rot[73] 39182492803516.59 1.8409761672048562E-13 30388840103.27773 +gas:Air@2.5bar310K.rot[74] 41287271729863.99 7.624908055047977E-14 30388840103.27773 +gas:Air@2.5bar310K.rot[75] 43385297539300.414 2.982964581244845E-14 30388840103.27773 +gas:Air@2.5bar310K.rot[76] 45476219420557.46 1.1030306148947663E-14 30388840103.27773 +gas:Air@2.5bar310K.rot[77] 47559686562367.14 3.85766358406034E-15 30388840103.27773 +gas:Air@2.5bar310K.rot[78] 49635348153461.5 1.276755338855168E-15 30388840103.27773 +gas:Air@2.5bar310K.rot[79] 51702853382572.15 4.001004534131757E-16 30388840103.27773 +gas:Air@2.5bar310K.rot[7] 12736646756509.334 3.104688963974321E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[80] 53761851438431.26 1.1877516459091432E-16 30388840103.27773 +gas:Air@2.5bar310K.rot[81] 55811991509770.43 3.341821134958247E-17 30388840103.27773 +gas:Air@2.5bar310K.rot[82] 57852922785321.84 8.915346878284336E-18 30388840103.27773 +gas:Air@2.5bar310K.rot[83] 59884294453817.09 2.2562122427044878E-18 30388840103.27773 +gas:Air@2.5bar310K.rot[84] 61905755703988.36 5.418657435461937E-19 30388840103.27773 +gas:Air@2.5bar310K.rot[85] 63916955724567.35 1.2355272676602613E-19 30388840103.27773 +gas:Air@2.5bar310K.rot[86] 65917543704285.9 2.67569911505843E-20 30388840103.27773 +gas:Air@2.5bar310K.rot[87] 67907168831875.95 5.5057866943610036E-21 30388840103.27773 +gas:Air@2.5bar310K.rot[88] 69885480296069.8 1.0768860300573938E-21 30388840103.27773 +gas:Air@2.5bar310K.rot[8] 14233591529527.81 6.579691149431157E-11 24311072082.62218 +gas:Air@2.5bar310K.rot[9] 15730041550159.912 3.3466397842328115E-11 24311072082.62218 +gas:Air@2.5bar310K.sigma 0 +gas:Ar@1bar293K.fR 0 +gas:Ar@1bar293K.indexReference vacuum +gas:Ar@1bar293K.isGas True +gas:Ar@1bar293K.n2 9.312638580931265E-24 +gas:Ar@1bar293K.n[0] 1.0002637279179725 +gas:Ar@1bar293K.n[10] 1.0002561825917031 +gas:Ar@1bar293K.n[11] 1.0002560550234236 +gas:Ar@1bar293K.n[12] 1.000255948593365 +gas:Ar@1bar293K.n[13] 1.0002558588704076 +gas:Ar@1bar293K.n[14] 1.0002557825279135 +gas:Ar@1bar293K.n[15] 1.0002557170287472 +gas:Ar@1bar293K.n[16] 1.000255660410427 +gas:Ar@1bar293K.n[17] 1.0002556111355816 +gas:Ar@1bar293K.n[18] 1.0002555679859542 +gas:Ar@1bar293K.n[19] 1.0002555299860303 +gas:Ar@1bar293K.n[1] 1.0002612575267105 +gas:Ar@1bar293K.n[20] 1.0002554963471844 +gas:Ar@1bar293K.n[21] 1.0002554664262762 +gas:Ar@1bar293K.n[22] 1.0002554396945715 +gas:Ar@1bar293K.n[23] 1.0002554157141479 +gas:Ar@1bar293K.n[2] 1.0002597202327768 +gas:Ar@1bar293K.n[3] 1.0002586966476648 +gas:Ar@1bar293K.n[4] 1.0002579799943738 +gas:Ar@1bar293K.n[5] 1.0002574583275787 +gas:Ar@1bar293K.n[6] 1.0002570666041257 +gas:Ar@1bar293K.n[7] 1.0002567648751697 +gas:Ar@1bar293K.n[8] 1.0002565274784607 +gas:Ar@1bar293K.n[9] 1.0002563373061768 +gas:Ar@1bar293K.sigma 0 +gas:Ar@2.5bar310K.fR 0 +gas:Ar@2.5bar310K.indexReference vacuum +gas:Ar@2.5bar310K.isGas True +gas:Ar@2.5bar310K.n2 2.2016129032258063E-23 +gas:Ar@2.5bar310K.n[0] 1.0006233704933143 +gas:Ar@2.5bar310K.n[10] 1.0006055388098003 +gas:Ar@2.5bar310K.n[11] 1.0006052373292555 +gas:Ar@2.5bar310K.n[12] 1.000604985804368 +gas:Ar@2.5bar310K.n[13] 1.000604773763145 +gas:Ar@2.5bar310K.n[14] 1.0006045933437948 +gas:Ar@2.5bar310K.n[15] 1.0006044385503434 +gas:Ar@2.5bar310K.n[16] 1.0006043047448858 +gas:Ar@2.5bar310K.n[17] 1.0006041882941739 +gas:Ar@2.5bar310K.n[18] 1.0006040863191172 +gas:Ar@2.5bar310K.n[19] 1.000603996514295 +gas:Ar@2.5bar310K.n[1] 1.0006175322987398 +gas:Ar@2.5bar310K.n[20] 1.000603917015959 +gas:Ar@2.5bar310K.n[21] 1.0006038463041835 +gas:Ar@2.5bar310K.n[22] 1.0006037831294177 +gas:Ar@2.5bar310K.n[23] 1.0006037264567242 +gas:Ar@2.5bar310K.n[2] 1.0006138992525346 +gas:Ar@2.5bar310K.n[3] 1.000611480236619 +gas:Ar@2.5bar310K.n[4] 1.0006097865838384 +gas:Ar@2.5bar310K.n[5] 1.0006085537378102 +gas:Ar@2.5bar310K.n[6] 1.0006076279840375 +gas:Ar@2.5bar310K.n[7] 1.000606914912484 +gas:Ar@2.5bar310K.n[8] 1.0006063538761665 +gas:Ar@2.5bar310K.n[9] 1.0006059044445408 +gas:Ar@2.5bar310K.sigma 0 +gas:CO2@1bar293K.fR 0 +gas:CO2@1bar293K.indexReference vacuum +gas:CO2@1bar293K.isGas True +gas:CO2@1bar293K.n2 1.303769401330377E-23 +gas:CO2@1bar293K.n[0] 1.00042230203346 +gas:CO2@1bar293K.n[10] 1.0004060670976973 +gas:CO2@1bar293K.n[11] 1.0004055660104942 +gas:CO2@1bar293K.n[12] 1.000405085228883 +gas:CO2@1bar293K.n[13] 1.0004046148719996 +gas:CO2@1bar293K.n[14] 1.0004041470747331 +gas:CO2@1bar293K.n[15] 1.0004036753560206 +gas:CO2@1bar293K.n[16] 1.0004031941793046 +gas:CO2@1bar293K.n[17] 1.0004026986368817 +gas:CO2@1bar293K.n[18] 1.0004021842151236 +gas:CO2@1bar293K.n[19] 1.0004016466126184 +gas:CO2@1bar293K.n[1] 1.000417427262325 +gas:CO2@1bar293K.n[20] 1.0004010815924589 +gas:CO2@1bar293K.n[21] 1.0004004848555628 +gas:CO2@1bar293K.n[22] 1.0003998519253754 +gas:CO2@1bar293K.n[23] 1.0003991780363763 +gas:CO2@1bar293K.n[2] 1.0004143378134511 +gas:CO2@1bar293K.n[3] 1.0004122289027229 +gas:CO2@1bar293K.n[4] 1.0004107013202344 +gas:CO2@1bar293K.n[5] 1.000409537872914 +gas:CO2@1bar293K.n[6] 1.000408611758891 +gas:CO2@1bar293K.n[7] 1.0004078446611389 +gas:CO2@1bar293K.n[8] 1.0004071858968022 +gas:CO2@1bar293K.n[9] 1.0004066013226531 +gas:CO2@1bar293K.rot[0] 441010905225.598 1.0574632900169905E-12 20000000000 +gas:CO2@1bar293K.rot[10] 6319174149222.688 1.0131907253067312E-10 20000000000 +gas:CO2@1bar293K.rot[11] 6906580413224.388 1.0191232325680581E-10 20000000000 +gas:CO2@1bar293K.rot[12] 7493873640048.917 9.949450240804454E-11 20000000000 +gas:CO2@1bar293K.rot[13] 8081044209511.016 9.448582497307965E-11 20000000000 +gas:CO2@1bar293K.rot[14] 8668082501425.381 8.743508111364913E-11 20000000000 +gas:CO2@1bar293K.rot[15] 9254978895606.656 7.895211023814881E-11 20000000000 +gas:CO2@1bar293K.rot[16] 9841723771869.648 6.964600111678716E-11 20000000000 +gas:CO2@1bar293K.rot[17] 10428307510029.03 6.007482868162053E-11 20000000000 +gas:CO2@1bar293K.rot[18] 11014720489899.459 5.0710192252731245E-11 20000000000 +gas:CO2@1bar293K.rot[19] 11600953091295.746 4.191734052631387E-11 20000000000 +gas:CO2@1bar293K.rot[1] 1029018430807.9545 6.2245346774726894E-12 20000000000 +gas:CO2@1bar293K.rot[20] 12186995694032.568 3.394960936080712E-11 20000000000 +gas:CO2@1bar293K.rot[21] 12772838677924.52 2.6954469750160678E-11 20000000000 +gas:CO2@1bar293K.rot[22] 13358472422786.488 2.098774812435332E-11 20000000000 +gas:CO2@1bar293K.rot[23] 13943887308433.043 1.6032477772127736E-11 20000000000 +gas:CO2@1bar293K.rot[24] 14529073714678.984 1.2019223665839236E-11 20000000000 +gas:CO2@1bar293K.rot[25] 15114022021338.953 8.845410310248434E-12 20000000000 +gas:CO2@1bar293K.rot[26] 15698722608227.752 6.391993167069346E-12 20000000000 +gas:CO2@1bar293K.rot[27] 16283165855159.932 4.536600262762986E-12 20000000000 +gas:CO2@1bar293K.rot[28] 16867342141950.47 3.1629303702866614E-12 20000000000 +gas:CO2@1bar293K.rot[29] 17451241848413.717 2.166675851281538E-12 20000000000 +gas:CO2@1bar293K.rot[2] 1617009121066.053 1.5034523024427247E-11 20000000000 +gas:CO2@1bar293K.rot[30] 18034855354364.734 1.4585327303200123E-12 20000000000 +gas:CO2@1bar293K.rot[31] 18618173039617.902 9.649892981586936E-13 20000000000 +gas:CO2@1bar293K.rot[32] 19201185283988.254 6.275860864447505E-13 20000000000 +gas:CO2@1bar293K.rot[33] 19783882467290.227 4.0125825392869704E-13 20000000000 +gas:CO2@1bar293K.rot[34] 20366254969338.766 2.5224667209940294E-13 20000000000 +gas:CO2@1bar293K.rot[35] 20948293169948.367 1.5592821759190666E-13 20000000000 +gas:CO2@1bar293K.rot[36] 21529987448933.863 9.479061000041183E-14 20000000000 +gas:CO2@1bar293K.rot[37] 22111328186109.977 5.667449969389677E-14 20000000000 +gas:CO2@1bar293K.rot[38] 22692305761291.258 3.3329555898813606E-14 20000000000 +gas:CO2@1bar293K.rot[39] 23272910554292.543 1.9280843531741725E-14 20000000000 +gas:CO2@1bar293K.rot[3] 2204973355814.601 2.670249481345725E-11 20000000000 +gas:CO2@1bar293K.rot[40] 23853132944928.773 1.0972616787462307E-14 20000000000 +gas:CO2@1bar293K.rot[41] 24432963313014.223 6.143460843181738E-15 20000000000 +gas:CO2@1bar293K.rot[42] 25012392038363.66 3.3842615055553723E-15 20000000000 +gas:CO2@1bar293K.rot[43] 25591409500792.215 1.834384177103789E-15 20000000000 +gas:CO2@1bar293K.rot[44] 26170006080114.258 9.784044261798981E-16 20000000000 +gas:CO2@1bar293K.rot[45] 26748172156144.285 5.135386150954867E-16 20000000000 +gas:CO2@1bar293K.rot[46] 27325898108697.7 2.6526383520113857E-16 20000000000 +gas:CO2@1bar293K.rot[47] 27903174317588.312 1.3485150563998484E-16 20000000000 +gas:CO2@1bar293K.rot[48] 28479991162631.754 6.747268014045225E-17 20000000000 +gas:CO2@1bar293K.rot[49] 29056339023641.95 3.3228843260116914E-17 20000000000 +gas:CO2@1bar293K.rot[4] 2792901514868.3125 4.021191382219064E-11 20000000000 +gas:CO2@1bar293K.rot[50] 29632208280433.96 1.610784576435155E-17 20000000000 +gas:CO2@1bar293K.rot[51] 30207589312822.85 7.68623773754052E-18 20000000000 +gas:CO2@1bar293K.rot[52] 30782472500622.426 3.610461600052317E-18 20000000000 +gas:CO2@1bar293K.rot[53] 31356848223648.438 1.669560070129883E-18 20000000000 +gas:CO2@1bar293K.rot[54] 31930706861714.918 7.600620351802592E-19 20000000000 +gas:CO2@1bar293K.rot[55] 32504038794636.363 3.4065946136465757E-19 20000000000 +gas:CO2@1bar293K.rot[56] 33076834402228.516 1.503254716243138E-19 20000000000 +gas:CO2@1bar293K.rot[57] 33649084064305.074 6.53132428540711E-20 20000000000 +gas:CO2@1bar293K.rot[58] 34220778160681.1 2.794099873172849E-20 20000000000 +gas:CO2@1bar293K.rot[59] 34791907071171.316 1.1769816779317199E-20 20000000000 +gas:CO2@1bar293K.rot[5] 3380783978041.893 5.442369878280872E-11 20000000000 +gas:CO2@1bar293K.rot[60] 35362461175590.44 4.882017590846223E-21 20000000000 +gas:CO2@1bar293K.rot[61] 35932430853753.19 1.9940925009923976E-21 20000000000 +gas:CO2@1bar293K.rot[6] 3968611125150.052 6.819325737546979E-11 20000000000 +gas:CO2@1bar293K.rot[7] 4556373336007.503 8.04807333522539E-11 20000000000 +gas:CO2@1bar293K.rot[8] 5144060990428.952 9.044160059590358E-11 20000000000 +gas:CO2@1bar293K.rot[9] 5731664468229.1045 9.748839613976843E-11 20000000000 +gas:CO2@1bar293K.sigma 0 +gas:CO2@2.5bar310K.fR 0 +gas:CO2@2.5bar310K.indexReference vacuum +gas:CO2@2.5bar310K.isGas True +gas:CO2@2.5bar310K.n2 3.082258064516129E-23 +gas:CO2@2.5bar310K.n[0] 1.0009983696863614 +gas:CO2@2.5bar310K.n[10] 1.0009599884652414 +gas:CO2@2.5bar310K.n[11] 1.0009588038385195 +gas:CO2@2.5bar310K.n[12] 1.0009576672165086 +gas:CO2@2.5bar310K.n[13] 1.0009565552397313 +gas:CO2@2.5bar310K.n[14] 1.0009554493141775 +gas:CO2@2.5bar310K.n[15] 1.0009543341178828 +gas:CO2@2.5bar310K.n[16] 1.0009531965617993 +gas:CO2@2.5bar310K.n[17] 1.0009520250435633 +gas:CO2@2.5bar310K.n[18] 1.0009508088924477 +gas:CO2@2.5bar310K.n[19] 1.000949537939428 +gas:CO2@2.5bar310K.n[1] 1.0009868451770205 +gas:CO2@2.5bar310K.n[20] 1.000948202167978 +gas:CO2@2.5bar310K.n[21] 1.0009467914145826 +gas:CO2@2.5bar310K.n[22] 1.0009452950961595 +gas:CO2@2.5bar310K.n[23] 1.0009437019464817 +gas:CO2@2.5bar310K.n[2] 1.0009795413710743 +gas:CO2@2.5bar310K.n[3] 1.0009745556680096 +gas:CO2@2.5bar310K.n[4] 1.0009709442905381 +gas:CO2@2.5bar310K.n[5] 1.000968193769716 +gas:CO2@2.5bar310K.n[6] 1.0009660043316042 +gas:CO2@2.5bar310K.n[7] 1.0009641908259104 +gas:CO2@2.5bar310K.n[8] 1.0009626334326416 +gas:CO2@2.5bar310K.n[9] 1.000961251433353 +gas:CO2@2.5bar310K.rot[0] 441010905225.598 2.23630722135437E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[10] 6319174149222.688 2.247494070707016E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[11] 6906580413224.388 2.2818724164905163E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[12] 7493873640048.917 2.2505124435685899E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[13] 8081044209511.016 2.1608616924042121E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[14] 8668082501425.381 2.0234115618128844E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[15] 9254978895606.656 1.8503785708589153E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[16] 9841723771869.648 1.6544415237400746E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[17] 10428307510029.03 1.4476584882647765E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[18] 11014720489899.459 1.240643591664874E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[19] 11600953091295.746 1.0420377413500737E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[1] 1029018430807.9545 1.3177226795467462E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[20] 12186995694032.568 8.582663693453189E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[21] 12772838677924.52 6.935459046685199E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[22] 13358472422786.488 5.500811500424289E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[23] 13943887308433.043 4.2838803525096636E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[24] 14529073714678.984 3.2767854058059654E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[25] 15114022021338.953 2.4625409096061566E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[26] 15698722608227.752 1.8186725116875422E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[27] 16283165855159.932 1.3202620317524864E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[28] 16867342141950.47 9.423000250731347E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[29] 17451241848413.717 6.613354940702361E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[2] 1617009121066.053 3.18873560470833E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[30] 18034855354364.734 4.564892796025233E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[31] 18618173039617.902 3.0994271655080474E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[32] 19201185283988.254 2.0703063688831143E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[33] 19783882467290.227 1.3606465172847958E-12 48622144165.24436 +gas:CO2@2.5bar310K.rot[34] 20366254969338.766 8.799626963281948E-13 48622144165.24436 +gas:CO2@2.5bar310K.rot[35] 20948293169948.367 5.600647892773011E-13 48622144165.24436 +gas:CO2@2.5bar310K.rot[36] 21529987448933.863 3.5084126215485946E-13 48622144165.24436 +gas:CO2@2.5bar310K.rot[37] 22111328186109.977 2.163324527778022E-13 48622144165.24436 +gas:CO2@2.5bar310K.rot[38] 22692305761291.258 1.3131331446344315E-13 48622144165.24436 +gas:CO2@2.5bar310K.rot[39] 23272910554292.543 7.84705913912839E-14 48622144165.24436 +gas:CO2@2.5bar310K.rot[3] 2204973355814.601 5.6787555273033007E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[40] 23853132944928.773 4.6168837497911177E-14 48622144165.24436 +gas:CO2@2.5bar310K.rot[41] 24432963313014.223 2.6746415854861964E-14 48622144165.24436 +gas:CO2@2.5bar310K.rot[42] 25012392038363.66 1.5257587931244668E-14 48622144165.24436 +gas:CO2@2.5bar310K.rot[43] 25591409500792.215 8.571112287718108E-15 48622144165.24436 +gas:CO2@2.5bar310K.rot[44] 26170006080114.258 4.741823884705677E-15 48622144165.24436 +gas:CO2@2.5bar310K.rot[45] 26748172156144.285 2.5836589288098996E-15 48622144165.24436 +gas:CO2@2.5bar310K.rot[46] 27325898108697.7 1.3865318946762173E-15 48622144165.24436 +gas:CO2@2.5bar310K.rot[47] 27903174317588.312 7.329118998253497E-16 48622144165.24436 +gas:CO2@2.5bar310K.rot[48] 28479991162631.754 3.8161217559067174E-16 48622144165.24436 +gas:CO2@2.5bar310K.rot[49] 29056339023641.95 1.957319061588565E-16 48622144165.24436 +gas:CO2@2.5bar310K.rot[4] 2792901514868.3125 8.582015292090308E-11 48622144165.24436 +gas:CO2@2.5bar310K.rot[50] 29632208280433.96 9.889846985896194E-17 48622144165.24436 +gas:CO2@2.5bar310K.rot[51] 30207589312822.85 4.922944094663031E-17 48622144165.24436 +gas:CO2@2.5bar310K.rot[52] 30782472500622.426 2.41426966071402E-17 48622144165.24436 +gas:CO2@2.5bar310K.rot[53] 31356848223648.438 1.1665133731045224E-17 48622144165.24436 +gas:CO2@2.5bar310K.rot[54] 31930706861714.918 5.553332777297751E-18 48622144165.24436 +gas:CO2@2.5bar310K.rot[55] 32504038794636.363 2.6049185230084658E-18 48622144165.24436 +gas:CO2@2.5bar310K.rot[56] 33076834402228.516 1.2040014646882179E-18 48622144165.24436 +gas:CO2@2.5bar310K.rot[57] 33649084064305.074 5.483626078118869E-19 48622144165.24436 +gas:CO2@2.5bar310K.rot[58] 34220778160681.1 2.461113921647269E-19 48622144165.24436 +gas:CO2@2.5bar310K.rot[59] 34791907071171.316 1.0885124983403402E-19 48622144165.24436 +gas:CO2@2.5bar310K.rot[5] 3380783978041.893 1.1665853260711732E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[60] 35362461175590.44 4.7444662228722466E-20 48622144165.24436 +gas:CO2@2.5bar310K.rot[61] 35932430853753.19 2.038016260653934E-20 48622144165.24436 +gas:CO2@2.5bar310K.rot[62] 36501806485474.18 8.627959674039342E-21 48622144165.24436 +gas:CO2@2.5bar310K.rot[63] 37070578450568.47 3.599998483577411E-21 48622144165.24436 +gas:CO2@2.5bar310K.rot[6] 3968611125150.052 1.469347373110221E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[7] 4556373336007.503 1.7445772293008294E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[8] 5144060990428.952 1.9739781495815277E-10 48622144165.24436 +gas:CO2@2.5bar310K.rot[9] 5731664468229.1045 2.1441906751305695E-10 48622144165.24436 +gas:CO2@2.5bar310K.sigma 0 +gas:H2@1bar293K.fR 0 +gas:H2@1bar293K.indexReference vacuum +gas:H2@1bar293K.isGas True +gas:H2@1bar293K.n2 6.425720620842573E-24 +gas:H2@1bar293K.n[0] 1.0001311079927335 +gas:H2@1bar293K.n[10] 1.0001256122110478 +gas:H2@1bar293K.n[11] 1.0001255196313077 +gas:H2@1bar293K.n[12] 1.000125442400186 +gas:H2@1bar293K.n[13] 1.0001253772982628 +gas:H2@1bar293K.n[14] 1.0001253219091193 +gas:H2@1bar293K.n[15] 1.0001252743901676 +gas:H2@1bar293K.n[16] 1.000125233316389 +gas:H2@1bar293K.n[17] 1.0001251975716043 +gas:H2@1bar293K.n[18] 1.0001251662714268 +gas:H2@1bar293K.n[19] 1.0001251387077603 +gas:H2@1bar293K.n[1] 1.0001293041381338 +gas:H2@1bar293K.n[20] 1.000125114308218 +gas:H2@1bar293K.n[21] 1.0001250926060434 +gas:H2@1bar293K.n[22] 1.0001250732175373 +gas:H2@1bar293K.n[23] 1.000125055824922 +gas:H2@1bar293K.n[2] 1.0001281838931637 +gas:H2@1bar293K.n[3] 1.0001274389235275 +gas:H2@1bar293K.n[4] 1.0001269177710106 +gas:H2@1bar293K.n[5] 1.0001265386323996 +gas:H2@1bar293K.n[6] 1.00012625405417 +gas:H2@1bar293K.n[7] 1.0001260349242411 +gas:H2@1bar293K.n[8] 1.0001258625575369 +gas:H2@1bar293K.n[9] 1.0001257245056607 +gas:H2@1bar293K.rot[0] 66725795046668.76 5.3490131781662795E-11 3333333333.3333335 +gas:H2@1bar293K.rot[1] 110499898500117.75 1.8493397082165407E-10 3333333333.3333335 +gas:H2@1bar293K.rot[2] 153209362087071.5 2.8790300298538214E-11 3333333333.3333335 +gas:H2@1bar293K.rot[3] 194428329860931.75 2.0257511933906277E-11 3333333333.3333335 +gas:H2@1bar293K.rot[4] 233730945875100.5 8.596178054161567E-13 3333333333.3333335 +gas:H2@1bar293K.rot[5] 270691354182979.66 1.867904121495036E-13 3333333333.3333335 +gas:H2@1bar293K.rot[6] 304883698837971 2.6707881683453916E-15 3333333333.3333335 +gas:H2@1bar293K.rot[7] 335882123893476.56 2.119023149408349E-16 3333333333.3333335 +gas:H2@1bar293K.rot[8] 363260773402898.1 1.202632573664938E-18 3333333333.3333335 +gas:H2@1bar293K.rot[9] 386593791419637.7 4.146514729197337E-20 3333333333.3333335 +gas:H2@1bar293K.sigma 0 +gas:H2@2.5bar310K.fR 0 +gas:H2@2.5bar310K.indexReference vacuum +gas:H2@2.5bar310K.isGas True +gas:H2@2.5bar310K.n2 1.5191129032258066E-23 +gas:H2@2.5bar310K.n[0] 1.0003099540973375 +gas:H2@2.5bar310K.n[10] 1.0002969614489408 +gas:H2@2.5bar310K.n[11] 1.0002967425799827 +gas:H2@2.5bar310K.n[12] 1.0002965599968912 +gas:H2@2.5bar310K.n[13] 1.0002964060885946 +gas:H2@2.5bar310K.n[14] 1.000296275142406 +gas:H2@2.5bar310K.n[15] 1.000296162802239 +gas:H2@2.5bar310K.n[16] 1.0002960656991888 +gas:H2@2.5bar310K.n[17] 1.0002959811944823 +gas:H2@2.5bar310K.n[18] 1.0002959071973287 +gas:H2@2.5bar310K.n[19] 1.0002958420337094 +gas:H2@2.5bar310K.n[1] 1.0003056895814026 +gas:H2@2.5bar310K.n[20] 1.0002957843504363 +gas:H2@2.5bar310K.n[21] 1.0002957330440452 +gas:H2@2.5bar310K.n[22] 1.0002956872074278 +gas:H2@2.5bar310K.n[23] 1.0002956460893213 +gas:H2@2.5bar310K.n[2] 1.000303041195814 +gas:H2@2.5bar310K.n[3] 1.0003012800034847 +gas:H2@2.5bar310K.n[4] 1.000300047940095 +gas:H2@2.5bar310K.n[5] 1.0002991516136124 +gas:H2@2.5bar310K.n[6] 1.0002984788385478 +gas:H2@2.5bar310K.n[7] 1.0002979607906557 +gas:H2@2.5bar310K.n[8] 1.0002975532963057 +gas:H2@2.5bar310K.n[9] 1.0002972269260841 +gas:H2@2.5bar310K.rot[0] 66725795046668.76 1.1760269587489347E-10 8103690694.207395 +gas:H2@2.5bar310K.rot[1] 110499898500117.75 4.243849167520953E-10 8103690694.207395 +gas:H2@2.5bar310K.rot[2] 153209362087071.5 7.074962374665487E-11 8103690694.207395 +gas:H2@2.5bar310K.rot[3] 194428329860931.75 5.48051780379153E-11 8103690694.207395 +gas:H2@2.5bar310K.rot[4] 233730945875100.5 2.633995742582334E-12 8103690694.207395 +gas:H2@2.5bar310K.rot[5] 270691354182979.66 6.666172054130004E-13 8103690694.207395 +gas:H2@2.5bar310K.rot[6] 304883698837971 1.1402869842645825E-14 8103690694.207395 +gas:H2@2.5bar310K.rot[7] 335882123893476.56 1.1099053239288965E-15 8103690694.207395 +gas:H2@2.5bar310K.rot[8] 363260773402898.1 7.908488956404895E-18 8103690694.207395 +gas:H2@2.5bar310K.rot[9] 386593791419637.7 3.4950501887012347E-19 8103690694.207395 +gas:H2@2.5bar310K.sigma 0 +gas:He@1bar293K.fR 0 +gas:He@1bar293K.indexReference vacuum +gas:He@1bar293K.isGas True +gas:He@1bar293K.n2 3.259423503325943E-25 +gas:He@1bar293K.n[0] 1.0000322689374752 +gas:He@1bar293K.n[10] 1.0000318804404364 +gas:He@1bar293K.n[11] 1.0000318738619158 +gas:He@1bar293K.n[12] 1.0000318683732052 +gas:He@1bar293K.n[13] 1.0000318637459145 +gas:He@1bar293K.n[14] 1.000031859808564 +gas:He@1bar293K.n[15] 1.0000318564303596 +gas:He@1bar293K.n[16] 1.0000318535101247 +gas:He@1bar293K.n[17] 1.000031850968594 +gas:He@1bar293K.n[18] 1.0000318487429523 +gas:He@1bar293K.n[19] 1.0000318467828986 +gas:He@1bar293K.n[1] 1.0000321418723626 +gas:He@1bar293K.n[20] 1.000031845047766 +gas:He@1bar293K.n[21] 1.0000318435043898 +gas:He@1bar293K.n[22] 1.0000318421255028 +gas:He@1bar293K.n[23] 1.000031840888521 +gas:He@1bar293K.n[2] 1.0000320627365653 +gas:He@1bar293K.n[3] 1.000032010017667 +gas:He@1bar293K.n[4] 1.0000319730939973 +gas:He@1bar293K.n[5] 1.0000319462097487 +gas:He@1bar293K.n[6] 1.0000319260184367 +gas:He@1bar293K.n[7] 1.000031910463694 +gas:He@1bar293K.n[8] 1.0000318982240775 +gas:He@1bar293K.n[9] 1.000031888418395 +gas:He@1bar293K.sigma 0 +gas:He@2.5bar310K.fR 0 +gas:He@2.5bar310K.indexReference vacuum +gas:He@2.5bar310K.isGas True +gas:He@2.5bar310K.n2 7.705645161290323E-25 +gas:He@2.5bar310K.n[0] 1.0000762857325591 +gas:He@2.5bar310K.n[10] 1.0000753673218776 +gas:He@2.5bar310K.n[11] 1.0000753517701881 +gas:He@2.5bar310K.n[12] 1.0000753387948207 +gas:He@2.5bar310K.n[13] 1.0000753278558585 +gas:He@2.5bar310K.n[14] 1.0000753185479219 +gas:He@2.5bar310K.n[15] 1.0000753105618123 +gas:He@2.5bar310K.n[16] 1.0000753036583472 +gas:He@2.5bar310K.n[17] 1.0000752976501426 +gas:He@2.5bar310K.n[18] 1.0000752923887033 +gas:He@2.5bar310K.n[19] 1.0000752877551162 +gas:He@2.5bar310K.n[1] 1.0000759853494818 +gas:He@2.5bar310K.n[20] 1.0000752836532454 +gas:He@2.5bar310K.n[21] 1.000075280004688 +gas:He@2.5bar310K.n[22] 1.0000752767449848 +gas:He@2.5bar310K.n[23] 1.000075273820747 +gas:He@2.5bar310K.n[2] 1.0000757982717143 +gas:He@2.5bar310K.n[3] 1.0000756736437324 +gas:He@2.5bar310K.n[4] 1.0000755863558177 +gas:He@2.5bar310K.n[5] 1.0000755228011893 +gas:He@2.5bar310K.n[6] 1.0000754750687273 +gas:He@2.5bar310K.n[7] 1.0000754382971606 +gas:He@2.5bar310K.n[8] 1.0000754093625839 +gas:He@2.5bar310K.n[9] 1.0000753861818517 +gas:He@2.5bar310K.sigma 0 +gas:Kr@1bar293K.fR 0 +gas:Kr@1bar293K.indexReference vacuum +gas:Kr@1bar293K.isGas True +gas:Kr@1bar293K.n2 2.607538802660754E-23 +gas:Kr@1bar293K.n[0] 1.0004043842929298 +gas:Kr@1bar293K.n[10] 1.0003888497893212 +gas:Kr@1bar293K.n[11] 1.0003885903531156 +gas:Kr@1bar293K.n[12] 1.0003883739865254 +gas:Kr@1bar293K.n[13] 1.0003881916415909 +gas:Kr@1bar293K.n[14] 1.0003880365309745 +gas:Kr@1bar293K.n[15] 1.0003879034816177 +gas:Kr@1bar293K.n[16] 1.0003877884944254 +gas:Kr@1bar293K.n[17] 1.0003876884381175 +gas:Kr@1bar293K.n[18] 1.0003876008324324 +gas:Kr@1bar293K.n[19] 1.0003875236920459 +gas:Kr@1bar293K.n[1] 1.000399256867088 +gas:Kr@1bar293K.n[20] 1.0003874554125045 +gas:Kr@1bar293K.n[21] 1.0003873946857182 +gas:Kr@1bar293K.n[22] 1.0003873404365633 +gas:Kr@1bar293K.n[23] 1.0003872917747731 +gas:Kr@1bar293K.n[2] 1.0003960865346706 +gas:Kr@1bar293K.n[3] 1.000393984237055 +gas:Kr@1bar293K.n[4] 1.0003925164185805 +gas:Kr@1bar293K.n[5] 1.0003914500728102 +gas:Kr@1bar293K.n[6] 1.0003906505119609 +gas:Kr@1bar293K.n[7] 1.0003900353229602 +gas:Kr@1bar293K.n[8] 1.0003895517156274 +gas:Kr@1bar293K.n[9] 1.0003891645744853 +gas:Kr@1bar293K.sigma 0 +gas:Kr@2.5bar310K.fR 0 +gas:Kr@2.5bar310K.indexReference vacuum +gas:Kr@2.5bar310K.isGas True +gas:Kr@2.5bar310K.n2 6.164516129032258E-23 +gas:Kr@2.5bar310K.n[0] 1.000955746696642 +gas:Kr@2.5bar310K.n[10] 1.0009190412178222 +gas:Kr@2.5bar310K.n[11] 1.00091842820612 +gas:Kr@2.5bar310K.n[12] 1.0009179169617681 +gas:Kr@2.5bar310K.n[13] 1.0009174861057701 +gas:Kr@2.5bar310K.n[14] 1.000917119600636 +gas:Kr@2.5bar310K.n[15] 1.0009168052231736 +gas:Kr@2.5bar310K.n[16] 1.0009165335240937 +gas:Kr@2.5bar310K.n[17] 1.0009162971046262 +gas:Kr@2.5bar310K.n[18] 1.0009160901042629 +gas:Kr@2.5bar310K.n[19] 1.0009159078319616 +gas:Kr@2.5bar310K.n[1] 1.0009436315179085 +gas:Kr@2.5bar310K.n[20] 1.0009157464966236 +gas:Kr@2.5bar310K.n[21] 1.0009156030074136 +gas:Kr@2.5bar310K.n[22] 1.000915474823963 +gas:Kr@2.5bar310K.n[23] 1.000915359842695 +gas:Kr@2.5bar310K.n[2] 1.000936140554226 +gas:Kr@2.5bar310K.n[3] 1.0009311731597956 +gas:Kr@2.5bar310K.n[4] 1.000927704930095 +gas:Kr@2.5bar310K.n[5] 1.0009251853144112 +gas:Kr@2.5bar310K.n[6] 1.0009232960692238 +gas:Kr@2.5bar310K.n[7] 1.000921842466313 +gas:Kr@2.5bar310K.n[8] 1.000920699771058 +gas:Kr@2.5bar310K.n[9] 1.0009197850111313 +gas:Kr@2.5bar310K.sigma 0 +gas:N2@1bar293K.fR 0 +gas:N2@1bar293K.indexReference vacuum +gas:N2@1bar293K.isGas True +gas:N2@1bar293K.n2 7.450110864745011E-24 +gas:N2@1bar293K.n[0] 1.0002794751981672 +gas:N2@1bar293K.n[10] 1.000271265603557 +gas:N2@1bar293K.n[11] 1.00027112990679 +gas:N2@1bar293K.n[12] 1.0002710167701654 +gas:N2@1bar293K.n[13] 1.0002709214463235 +gas:N2@1bar293K.n[14] 1.0002708403762504 +gas:N2@1bar293K.n[15] 1.0002707708488223 +gas:N2@1bar293K.n[16] 1.0002707107691082 +gas:N2@1bar293K.n[17] 1.0002706584974115 +gas:N2@1bar293K.n[18] 1.0002706127353702 +gas:N2@1bar293K.n[19] 1.0002705724440106 +gas:N2@1bar293K.n[1] 1.0002767462517559 +gas:N2@1bar293K.n[20] 1.0002705367838904 +gas:N2@1bar293K.n[21] 1.0002705050707743 +gas:N2@1bar293K.n[22] 1.0002704767423982 +gas:N2@1bar293K.n[23] 1.000270451333254 +gas:N2@1bar293K.n[2] 1.0002750686945565 +gas:N2@1bar293K.n[3] 1.0002739602213382 +gas:N2@1bar293K.n[4] 1.0002731880968705 +gas:N2@1bar293K.n[5] 1.0002726280742595 +gas:N2@1bar293K.n[6] 1.00027220865777 +gas:N2@1bar293K.n[7] 1.000271886241188 +gas:N2@1bar293K.n[8] 1.000271632958772 +gas:N2@1bar293K.n[9] 1.0002714303081348 +gas:N2@1bar293K.rot[0] 2248554931241.2383 1.9835359000937137E-12 10000000000 +gas:N2@1bar293K.rot[10] 17225944739207.072 3.708126151817012E-11 10000000000 +gas:N2@1bar293K.rot[11] 18721249017470.758 1.736122273393724E-11 10000000000 +gas:N2@1bar293K.rot[12] 20215902305752.38 3.148130660131386E-11 10000000000 +gas:N2@1bar293K.rot[13] 21709852524853.36 1.3843963855168148E-11 10000000000 +gas:N2@1bar293K.rot[14] 23203047595575.168 2.3657575832170575E-11 10000000000 +gas:N2@1bar293K.rot[15] 24695435438719.223 9.830779986343695E-12 10000000000 +gas:N2@1bar293K.rot[16] 26186963975086.938 1.5909892831214267E-11 10000000000 +gas:N2@1bar293K.rot[17] 27677581125479.77 6.272636641774835E-12 10000000000 +gas:N2@1bar293K.rot[18] 29167234810699.152 9.646333339008954E-12 10000000000 +gas:N2@1bar293K.rot[19] 30655872951546.543 3.6186315985296134E-12 10000000000 +gas:N2@1bar293K.rot[1] 3747504753404.4556 2.8619970681007358E-12 10000000000 +gas:N2@1bar293K.rot[20] 32143443468823.34 5.3007924881311545E-12 10000000000 +gas:N2@1bar293K.rot[21] 33629894283330.984 1.8959475029706414E-12 10000000000 +gas:N2@1bar293K.rot[22] 35115173315870.902 2.650276452546289E-12 10000000000 +gas:N2@1bar293K.rot[23] 36599228487244.516 9.052435723207548E-13 10000000000 +gas:N2@1bar293K.rot[24] 38082007718253.41 1.2092111329163177E-12 10000000000 +gas:N2@1bar293K.rot[25] 39563458929698.9 3.949116658320627E-13 10000000000 +gas:N2@1bar293K.rot[26] 41043530042382.28 5.046444048274979E-13 10000000000 +gas:N2@1bar293K.rot[27] 42522168977105.15 1.5773745514479607E-13 10000000000 +gas:N2@1bar293K.rot[28] 43999323654669.02 1.9299911837025708E-13 10000000000 +gas:N2@1bar293K.rot[29] 45474941995875.24 5.778386260440803E-14 10000000000 +gas:N2@1bar293K.rot[2] 5246324377571.26 1.080032096920913E-11 10000000000 +gas:N2@1bar293K.rot[30] 46948971921525.13 6.774566398683965E-14 10000000000 +gas:N2@1bar293K.rot[31] 48421361352420.23 1.944139070309764E-14 10000000000 +gas:N2@1bar293K.rot[32] 49892058209362.02 2.185375985427882E-14 10000000000 +gas:N2@1bar293K.rot[33] 51361010413151.92 6.014724839096866E-15 10000000000 +gas:N2@1bar293K.rot[34] 52828165884591.31 6.485895644285782E-15 10000000000 +gas:N2@1bar293K.rot[35] 54293472544481.67 1.7128494895537013E-15 10000000000 +gas:N2@1bar293K.rot[36] 55756878313624.31 1.7726823205498173E-15 10000000000 +gas:N2@1bar293K.rot[37] 57218331112820.71 4.493966924369276E-16 10000000000 +gas:N2@1bar293K.rot[38] 58677778862872.484 4.465586071429916E-16 10000000000 +gas:N2@1bar293K.rot[39] 60135169484580.92 1.0871688945315157E-16 10000000000 +gas:N2@1bar293K.rot[3] 6744961724543.089 8.327859009586461E-12 10000000000 +gas:N2@1bar293K.rot[40] 61590450898747.58 1.0376334442104806E-16 10000000000 +gas:N2@1bar293K.rot[41] 63043571026173.81 2.4268118088934568E-17 10000000000 +gas:N2@1bar293K.rot[42] 64494477787660.836 2.2255057889904997E-17 10000000000 +gas:N2@1bar293K.rot[43] 65943119104010.45 5.001909866203179E-18 10000000000 +gas:N2@1bar293K.rot[44] 67389442896023.88 4.408697550985351E-18 10000000000 +gas:N2@1bar293K.rot[45] 68833397084502.48 9.52497529665585E-19 10000000000 +gas:N2@1bar293K.rot[46] 70274929590247.914 8.0713716946944855E-19 10000000000 +gas:N2@1bar293K.rot[47] 71713988334061.53 1.6767596316476812E-19 10000000000 +gas:N2@1bar293K.rot[48] 73150521236744.78 1.3664177891470684E-19 10000000000 +gas:N2@1bar293K.rot[49] 74584476219099.25 2.730197612192152E-20 10000000000 +gas:N2@1bar293K.rot[4] 8243364715121.375 2.2672746982111764E-11 10000000000 +gas:N2@1bar293K.rot[50] 76015801201926.02 2.140181281356049E-20 10000000000 +gas:N2@1bar293K.rot[51] 77444444106026.45 4.1139640370671055E-21 10000000000 +gas:N2@1bar293K.rot[52] 78870352852202.34 3.1029225178396174E-21 10000000000 +gas:N2@1bar293K.rot[53] 80293475361255.27 5.73969187368829E-22 10000000000 +gas:N2@1bar293K.rot[54] 81713759553986.44 4.1663883523551297E-22 10000000000 +gas:N2@1bar293K.rot[5] 9741481270107.557 1.4126177910852149E-11 10000000000 +gas:N2@1bar293K.rot[6] 11239259310303.064 3.2884446176150194E-11 10000000000 +gas:N2@1bar293K.rot[7] 12736646756509.334 1.810000611309657E-11 10000000000 +gas:N2@1bar293K.rot[8] 14233591529527.81 3.7998920727852055E-11 10000000000 +gas:N2@1bar293K.rot[9] 15730041550159.912 1.9126050401642785E-11 10000000000 +gas:N2@1bar293K.sigma 0 +gas:N2@2.5bar310K.fR 0 +gas:N2@2.5bar310K.indexReference vacuum +gas:N2@2.5bar310K.isGas True +gas:N2@2.5bar310K.n2 1.761290322580645E-23 +gas:N2@2.5bar310K.n[0] 1.0006605850619643 +gas:N2@2.5bar310K.n[10] 1.0006411839368068 +gas:N2@2.5bar310K.n[11] 1.0006408632528938 +gas:N2@2.5bar310K.n[12] 1.0006405958839477 +gas:N2@2.5bar310K.n[13] 1.0006403706108422 +gas:N2@2.5bar310K.n[14] 1.0006401790227883 +gas:N2@2.5bar310K.n[15] 1.0006400147127603 +gas:N2@2.5bar310K.n[16] 1.0006398727299397 +gas:N2@2.5bar310K.n[17] 1.0006397491993326 +gas:N2@2.5bar310K.n[18] 1.0006396410526024 +gas:N2@2.5bar310K.n[19] 1.0006395458344055 +gas:N2@2.5bar310K.n[1] 1.0006541359696783 +gas:N2@2.5bar310K.n[20] 1.000639461560939 +gas:N2@2.5bar310K.n[21] 1.0006393866151928 +gas:N2@2.5bar310K.n[22] 1.0006393196684005 +gas:N2@2.5bar310K.n[23] 1.0006392596204565 +gas:N2@2.5bar310K.n[2] 1.000650171526253 +gas:N2@2.5bar310K.n[3] 1.0006475519507763 +gas:N2@2.5bar310K.n[4] 1.0006457272419091 +gas:N2@2.5bar310K.n[5] 1.0006444037776314 +gas:N2@2.5bar310K.n[6] 1.0006434125978094 +gas:N2@2.5bar310K.n[7] 1.0006426506512003 +gas:N2@2.5bar310K.n[8] 1.0006420520847366 +gas:N2@2.5bar310K.n[9] 1.000641573173034 +gas:N2@2.5bar310K.rot[0] 2248554931241.2383 4.200603284792411E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[10] 17225944739207.072 8.40624837211061E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[11] 18721249017470.758 3.985530578290921E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[12] 20215902305752.38 7.326065782823196E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[13] 21709852524853.36 3.269224024877029E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[14] 23203047595575.168 5.67511109022535E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[15] 24695435438719.223 2.398090348706871E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[16] 26186963975086.938 3.950677468884959E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[17] 27677581125479.77 1.58720964954515E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[18] 29167234810699.152 2.4898839933995916E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[19] 30655872951546.543 9.537762038118063E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[1] 3747504753404.4556 6.07366879269697E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[20] 32143443468823.34 1.4281719019487081E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[21] 33629894283330.984 5.227037031260433E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[22] 35115173315870.902 7.484482027990705E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[23] 36599228487244.516 2.621372406914994E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[24] 38082007718253.41 3.5942574430193935E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[25] 39563458929698.9 1.2061504959467663E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[26] 41043530042382.28 1.5853744516477566E-12 24311072082.62218 +gas:N2@2.5bar310K.rot[27] 42522168977105.15 5.10241840344116E-13 24311072082.62218 +gas:N2@2.5bar310K.rot[28] 43999323654669.02 6.434888265243785E-13 24311072082.62218 +gas:N2@2.5bar310K.rot[29] 45474941995875.24 1.9878604769291203E-13 24311072082.62218 +gas:N2@2.5bar310K.rot[2] 5246324377571.26 2.29923852151104E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[30] 46948971921525.13 2.407150581351866E-13 24311072082.62218 +gas:N2@2.5bar310K.rot[31] 48421361352420.23 7.142329086209705E-14 24311072082.62218 +gas:N2@2.5bar310K.rot[32] 49892058209362.02 8.30954740239716E-14 24311072082.62218 +gas:N2@2.5bar310K.rot[33] 51361010413151.92 2.36948018666489E-14 24311072082.62218 +gas:N2@2.5bar310K.rot[34] 52828165884591.31 2.6499663020106463E-14 24311072082.62218 +gas:N2@2.5bar310K.rot[35] 54293472544481.67 7.265560870594507E-15 24311072082.62218 +gas:N2@2.5bar310K.rot[36] 55756878313624.31 7.814593103409705E-15 24311072082.62218 +gas:N2@2.5bar310K.rot[37] 57218331112820.71 2.0609918987027388E-15 24311072082.62218 +gas:N2@2.5bar310K.rot[38] 58677778862872.484 2.1327538803593493E-15 24311072082.62218 +gas:N2@2.5bar310K.rot[39] 60135169484580.92 5.41276924034716E-16 24311072082.62218 +gas:N2@2.5bar310K.rot[3] 6744961724543.089 1.7803330637569205E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[40] 61590450898747.58 5.391014021013564E-16 24311072082.62218 +gas:N2@2.5bar310K.rot[41] 63043571026173.81 1.3170728916185441E-16 24311072082.62218 +gas:N2@2.5bar310K.rot[42] 64494477787660.836 1.2629677440354418E-16 24311072082.62218 +gas:N2@2.5bar310K.rot[43] 65943119104010.45 2.9711953443098274E-17 24311072082.62218 +gas:N2@2.5bar310K.rot[44] 67389442896023.88 2.7439666600825772E-17 24311072082.62218 +gas:N2@2.5bar310K.rot[45] 68833397084502.48 6.217936523612876E-18 24311072082.62218 +gas:N2@2.5bar310K.rot[46] 70274929590247.914 5.5320199551398675E-18 24311072082.62218 +gas:N2@2.5bar310K.rot[47] 71713988334061.53 1.2078153695768217E-18 24311072082.62218 +gas:N2@2.5bar310K.rot[48] 73150521236744.78 1.0354885406034658E-18 24311072082.62218 +gas:N2@2.5bar310K.rot[49] 74584476219099.25 2.178842857682178E-19 24311072082.62218 +gas:N2@2.5bar310K.rot[4] 8243364715121.375 4.8724522814253176E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[50] 76015801201926.02 1.800485764419291E-19 24311072082.62218 +gas:N2@2.5bar310K.rot[51] 77444444106026.45 3.6521066721600135E-20 24311072082.62218 +gas:N2@2.5bar310K.rot[52] 78870352852202.34 2.9096014267988195E-20 24311072082.62218 +gas:N2@2.5bar310K.rot[53] 80293475361255.27 5.690700892413377E-21 24311072082.62218 +gas:N2@2.5bar310K.rot[54] 81713759553986.44 4.372056421021085E-21 24311072082.62218 +gas:N2@2.5bar310K.rot[55] 83131153351196.95 8.247052181174013E-22 24311072082.62218 +gas:N2@2.5bar310K.rot[5] 9741481270107.557 3.054911597667113E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[6] 11239259310303.064 7.163906552577937E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[7] 12736646756509.334 3.976292218204818E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[8] 14233591529527.81 8.426858541791956E-11 24311072082.62218 +gas:N2@2.5bar310K.rot[9] 15730041550159.912 4.286167756445713E-11 24311072082.62218 +gas:N2@2.5bar310K.sigma 0 +gas:Ne@1bar293K.fR 0 +gas:Ne@1bar293K.indexReference vacuum +gas:Ne@1bar293K.isGas True +gas:Ne@1bar293K.n2 6.984478935698449E-25 +gas:Ne@1bar293K.n[0] 1.000062206781751 +gas:Ne@1bar293K.n[10] 1.0000614484766097 +gas:Ne@1bar293K.n[11] 1.0000614354135247 +gas:Ne@1bar293K.n[12] 1.0000614245086847 +gas:Ne@1bar293K.n[13] 1.0000614153111833 +gas:Ne@1bar293K.n[14] 1.0000614074820877 +gas:Ne@1bar293K.n[15] 1.0000614007626345 +gas:Ne@1bar293K.n[16] 1.0000613949524912 +gas:Ne@1bar293K.n[17] 1.0000613898946014 +gas:Ne@1bar293K.n[18] 1.0000613854644267 +gas:Ne@1bar293K.n[19] 1.0000613815621855 +gas:Ne@1bar293K.n[1] 1.0000619615622393 +gas:Ne@1bar293K.n[20] 1.0000613781071717 +gas:Ne@1bar293K.n[21] 1.000061375033538 +gas:Ne@1bar293K.n[22] 1.0000613722871294 +gas:Ne@1bar293K.n[23] 1.000061369823076 +gas:Ne@1bar293K.n[2] 1.000061807480771 +gas:Ne@1bar293K.n[3] 1.000061704246229 +gas:Ne@1bar293K.n[4] 1.0000616316591056 +gas:Ne@1bar293K.n[5] 1.0000615786605072 +gas:Ne@1bar293K.n[6] 1.0000615387738687 +gas:Ne@1bar293K.n[7] 1.00006150799818 +gas:Ne@1bar293K.n[8] 1.0000614837519666 +gas:Ne@1bar293K.n[9] 1.0000614643084087 +gas:Ne@1bar293K.sigma 0 +gas:Ne@2.5bar310K.fR 0 +gas:Ne@2.5bar310K.indexReference vacuum +gas:Ne@2.5bar310K.isGas True +gas:Ne@2.5bar310K.n2 1.651209677419355E-24 +gas:Ne@2.5bar310K.n[0] 1.0001470576166194 +gas:Ne@2.5bar310K.n[10] 1.0001452650488143 +gas:Ne@2.5bar310K.n[11] 1.0001452341687942 +gas:Ne@2.5bar310K.n[12] 1.0001452083906814 +gas:Ne@2.5bar310K.n[13] 1.0001451866485709 +gas:Ne@2.5bar310K.n[14] 1.0001451681412552 +gas:Ne@2.5bar310K.n[15] 1.0001451522570395 +gas:Ne@2.5bar310K.n[16] 1.000145138522355 +gas:Ne@2.5bar310K.n[17] 1.0001451265659338 +gas:Ne@2.5bar310K.n[18] 1.0001451160933774 +gas:Ne@2.5bar310K.n[19] 1.000145106868811 +gas:Ne@2.5bar310K.n[1] 1.0001464779390932 +gas:Ne@2.5bar310K.n[20] 1.0001450987014517 +gas:Ne@2.5bar310K.n[21] 1.0001450914356431 +gas:Ne@2.5bar310K.n[22] 1.0001450849433664 +gas:Ne@2.5bar310K.n[23] 1.0001450791185535 +gas:Ne@2.5bar310K.n[2] 1.0001461137038496 +gas:Ne@2.5bar310K.n[3] 1.0001458696662922 +gas:Ne@2.5bar310K.n[4] 1.0001456980765697 +gas:Ne@2.5bar310K.n[5] 1.0001455727924262 +gas:Ne@2.5bar310K.n[6] 1.0001454785038262 +gas:Ne@2.5bar310K.n[7] 1.0001454057527288 +gas:Ne@2.5bar310K.n[8] 1.00014534843675 +gas:Ne@2.5bar310K.n[9] 1.0001453024738378 +gas:Ne@2.5bar310K.sigma 0 +gas:O2@1bar293K.fR 0 +gas:O2@1bar293K.indexReference vacuum +gas:O2@1bar293K.isGas True +gas:O2@1bar293K.n2 9.592017738359203E-24 +gas:O2@1bar293K.n[0] 1.000255172286224 +gas:O2@1bar293K.n[10] 1.0002450945792944 +gas:O2@1bar293K.n[11] 1.0002449294996207 +gas:O2@1bar293K.n[12] 1.0002447919046547 +gas:O2@1bar293K.n[13] 1.0002446760012895 +gas:O2@1bar293K.n[14] 1.000244577449125 +gas:O2@1bar293K.n[15] 1.0002444929434822 +gas:O2@1bar293K.n[16] 1.0002444199318872 +gas:O2@1bar293K.n[17] 1.0002443564172743 +gas:O2@1bar293K.n[18] 1.0002443008188204 +gas:O2@1bar293K.n[19] 1.0002442518718806 +gas:O2@1bar293K.n[1] 1.00025180342595 +gas:O2@1bar293K.n[20] 1.000244208554936 +gas:O2@1bar293K.n[21] 1.000244170035524 +gas:O2@1bar293K.n[22] 1.0002441356297098 +gas:O2@1bar293K.n[23] 1.000244104771352 +gas:O2@1bar293K.n[2] 1.0002497415887348 +gas:O2@1bar293K.n[3] 1.0002483831541746 +gas:O2@1bar293K.n[4] 1.000247438826021 +gas:O2@1bar293K.n[5] 1.0002467549035645 +gas:O2@1bar293K.n[6] 1.0002462432526615 +gas:O2@1bar293K.n[7] 1.0002458502608447 +gas:O2@1bar293K.n[8] 1.0002455417376044 +gas:O2@1bar293K.n[9] 1.0002452950172183 +gas:O2@1bar293K.rot[0] 2707960287006.391 1.2122529097475831E-11 12500000000 +gas:O2@1bar293K.rot[10] 24289421192094.09 3.05998848997259E-11 12500000000 +gas:O2@1bar293K.rot[11] 26431649221312.383 1.8784221173493194E-11 12500000000 +gas:O2@1bar293K.rot[12] 28569579812496.28 1.0762627520435618E-11 12500000000 +gas:O2@1bar293K.rot[13] 30702862154377.707 5.767562158470908E-12 12500000000 +gas:O2@1bar293K.rot[14] 32831145435688.547 2.8956305117780964E-12 12500000000 +gas:O2@1bar293K.rot[15] 34954078845160.746 1.3638728905290806E-12 12500000000 +gas:O2@1bar293K.rot[16] 37071311571526.16 6.033834568179285E-13 12500000000 +gas:O2@1bar293K.rot[17] 39182492803516.59 2.5097964928606165E-13 12500000000 +gas:O2@1bar293K.rot[18] 41287271729863.99 9.824044329902894E-14 12500000000 +gas:O2@1bar293K.rot[19] 43385297539300.414 3.621481266044845E-14 12500000000 +gas:O2@1bar293K.rot[1] 4873868076822.109 3.662142438334975E-11 12500000000 +gas:O2@1bar293K.rot[20] 45476219420557.46 1.2581408082653576E-14 12500000000 +gas:O2@1bar293K.rot[21] 47559686562367.14 4.121875592352584E-15 12500000000 +gas:O2@1bar293K.rot[22] 49635348153461.5 1.2741976249787517E-15 12500000000 +gas:O2@1bar293K.rot[23] 51702853382572.15 3.718699636530663E-16 12500000000 +gas:O2@1bar293K.rot[24] 53761851438431.26 1.0251321640686086E-16 12500000000 +gas:O2@1bar293K.rot[25] 55811991509770.43 2.6706307622340096E-17 12500000000 +gas:O2@1bar293K.rot[26] 57852922785321.84 6.578014819964373E-18 12500000000 +gas:O2@1bar293K.rot[27] 59884294453817.09 1.5325601002138038E-18 12500000000 +gas:O2@1bar293K.rot[28] 61905755703988.36 3.3788655536420895E-19 12500000000 +gas:O2@1bar293K.rot[29] 63916955724567.35 7.0524463696447E-20 12500000000 +gas:O2@1bar293K.rot[2] 7038986541284.577 6.58877579472491E-11 12500000000 +gas:O2@1bar293K.rot[30] 65917543704285.9 1.394137716754052E-20 12500000000 +gas:O2@1bar293K.rot[31] 67907168831875.95 2.611245797415885E-21 12500000000 +gas:O2@1bar293K.rot[3] 9202964869125.686 9.148200824009049E-11 12500000000 +gas:O2@1bar293K.rot[4] 11365452249077.332 1.070177068308747E-10 12500000000 +gas:O2@1bar293K.rot[5] 13526097869871.385 1.0986962355556724E-10 12500000000 +gas:O2@1bar293K.rot[6] 15684550920239.756 1.0123132137981397E-10 12500000000 +gas:O2@1bar293K.rot[7] 17840460588914.344 8.485659606245801E-11 12500000000 +gas:O2@1bar293K.rot[8] 19993476064626.973 6.529385440719513E-11 12500000000 +gas:O2@1bar293K.rot[9] 22143246536109.605 4.640525298728777E-11 12500000000 +gas:O2@1bar293K.sigma 0 +gas:O2@2.5bar310K.fR 0 +gas:O2@2.5bar310K.indexReference vacuum +gas:O2@2.5bar310K.isGas True +gas:O2@2.5bar310K.n2 2.2676612903225805E-23 +gas:O2@2.5bar310K.n[0] 1.0006032560944083 +gas:O2@2.5bar310K.n[10] 1.0005794312574205 +gas:O2@2.5bar310K.n[11] 1.0005790409904338 +gas:O2@2.5bar310K.n[12] 1.0005787157003991 +gas:O2@2.5bar310K.n[13] 1.0005784416917582 +gas:O2@2.5bar310K.n[14] 1.0005782087033148 +gas:O2@2.5bar310K.n[15] 1.0005780089224336 +gas:O2@2.5bar310K.n[16] 1.0005778363147801 +gas:O2@2.5bar310K.n[17] 1.000577686159064 +gas:O2@2.5bar310K.n[18] 1.000577554718042 +gas:O2@2.5bar310K.n[19] 1.00057743900195 +gas:O2@2.5bar310K.n[1] 1.000595291728365 +gas:O2@2.5bar310K.n[20] 1.0005773365958022 +gas:O2@2.5bar310K.n[21] 1.0005772455315636 +gas:O2@2.5bar310K.n[22] 1.000577164192334 +gas:O2@2.5bar310K.n[23] 1.000577091239692 +gas:O2@2.5bar310K.n[2] 1.0005904173124003 +gas:O2@2.5bar310K.n[3] 1.0005872058197283 +gas:O2@2.5bar310K.n[4] 1.000584973321355 +gas:O2@2.5bar310K.n[5] 1.0005833564514512 +gas:O2@2.5bar310K.n[6] 1.0005821468509495 +gas:O2@2.5bar310K.n[7] 1.0005812177739242 +gas:O2@2.5bar310K.n[8] 1.0005804883901512 +gas:O2@2.5bar310K.n[9] 1.0005799051153028 +gas:O2@2.5bar310K.rot[0] 2707960287006.391 2.5698384192352606E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[10] 24289421192094.09 7.841841098619783E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[11] 26431649221312.383 4.9882789327679206E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[12] 28569579812496.28 2.9705738387677376E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[13] 30702862154377.707 1.659516107761793E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[14] 32831145435688.547 8.71162849052686E-12 30388840103.27773 +gas:O2@2.5bar310K.rot[15] 34954078845160.746 4.3032244753349654E-12 30388840103.27773 +gas:O2@2.5bar310K.rot[16] 37071311571526.16 2.0024934728308015E-12 30388840103.27773 +gas:O2@2.5bar310K.rot[17] 39182492803516.59 8.787475738448002E-13 30388840103.27773 +gas:O2@2.5bar310K.rot[18] 41287271729863.99 3.63957425061956E-13 30388840103.27773 +gas:O2@2.5bar310K.rot[19] 43385297539300.414 1.4238494421216447E-13 30388840103.27773 +gas:O2@2.5bar310K.rot[1] 4873868076822.109 7.804735133653987E-11 30388840103.27773 +gas:O2@2.5bar310K.rot[20] 45476219420557.46 5.265062600929673E-14 30388840103.27773 +gas:O2@2.5bar310K.rot[21] 47559686562367.14 1.8413668658999238E-14 30388840103.27773 +gas:O2@2.5bar310K.rot[22] 49635348153461.5 6.094297560167868E-15 30388840103.27773 +gas:O2@2.5bar310K.rot[23] 51702853382572.15 1.9097873671273306E-15 30388840103.27773 +gas:O2@2.5bar310K.rot[24] 53761851438431.26 5.6694589303539065E-16 30388840103.27773 +gas:O2@2.5bar310K.rot[25] 55811991509770.43 1.5951413532020273E-16 30388840103.27773 +gas:O2@2.5bar310K.rot[26] 57852922785321.84 4.2555355027610205E-17 30388840103.27773 +gas:O2@2.5bar310K.rot[27] 59884294453817.09 1.0769509511715934E-17 30388840103.27773 +gas:O2@2.5bar310K.rot[28] 61905755703988.36 2.586471329576104E-18 30388840103.27773 +gas:O2@2.5bar310K.rot[29] 63916955724567.35 5.89750485756688E-19 30388840103.27773 +gas:O2@2.5bar310K.rot[2] 7038986541284.577 1.415982253412671E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[30] 65917543704285.9 1.2771833484765777E-19 30388840103.27773 +gas:O2@2.5bar310K.rot[31] 67907168831875.95 2.6280604746353246E-20 30388840103.27773 +gas:O2@2.5bar310K.rot[32] 69885480296069.8 5.140267446574673E-21 30388840103.27773 +gas:O2@2.5bar310K.rot[3] 9202964869125.686 1.9885574940524374E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[4] 11365452249077.332 2.3600758964792563E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[5] 13526097869871.385 2.4656609879762815E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[6] 15684550920239.756 2.31884868080177E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[7] 17840460588914.344 1.990031303671955E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[8] 19993476064626.973 1.57245009413203E-10 30388840103.27773 +gas:O2@2.5bar310K.rot[9] 22143246536109.605 1.1511016522740008E-10 30388840103.27773 +gas:O2@2.5bar310K.sigma 0 +gas:Xe@1bar293K.fR 0 +gas:Xe@1bar293K.indexReference vacuum +gas:Xe@1bar293K.isGas True +gas:Xe@1bar293K.n2 7.450110864745012E-23 +gas:Xe@1bar293K.n[0] 1.0006703754092385 +gas:Xe@1bar293K.n[10] 1.0006332873634058 +gas:Xe@1bar293K.n[11] 1.0006326773992977 +gas:Xe@1bar293K.n[12] 1.000632168929076 +gas:Xe@1bar293K.n[13] 1.0006317405750451 +gas:Xe@1bar293K.n[14] 1.0006313763162173 +gas:Xe@1bar293K.n[15] 1.000631063951969 +gas:Xe@1bar293K.n[16] 1.000630794057039 +gas:Xe@1bar293K.n[17] 1.0006305592558977 +gas:Xe@1bar293K.n[18] 1.0006303537094827 +gas:Xe@1bar293K.n[19] 1.0006301727460343 +gas:Xe@1bar293K.n[1] 1.0006580094620368 +gas:Xe@1bar293K.n[20] 1.0006300125915384 +gas:Xe@1bar293K.n[21] 1.0006298701701672 +gas:Xe@1bar293K.n[22] 1.0006297429546736 +gas:Xe@1bar293K.n[23] 1.0006296288529237 +gas:Xe@1bar293K.n[2] 1.0006504253590245 +gas:Xe@1bar293K.n[3] 1.0006454219636716 +gas:Xe@1bar293K.n[4] 1.0006419406774285 +gas:Xe@1bar293K.n[5] 1.0006394177702025 +gas:Xe@1bar293K.n[6] 1.0006375294625867 +gas:Xe@1bar293K.n[7] 1.000636078561765 +gas:Xe@1bar293K.n[8] 1.000634939197165 +gas:Xe@1bar293K.n[9] 1.0006340278675152 +gas:Xe@1bar293K.sigma 0 +gas:Xe@2.5bar310K.fR 0 +gas:Xe@2.5bar310K.indexReference vacuum +gas:Xe@2.5bar310K.isGas True +gas:Xe@2.5bar310K.n2 1.761290322580645E-22 +gas:Xe@2.5bar310K.n[0] 1.001584119657381 +gas:Xe@2.5bar310K.n[10] 1.001496517112693 +gas:Xe@2.5bar310K.n[11] 1.0014950763310153 +gas:Xe@2.5bar310K.n[12] 1.001493875284714 +gas:Xe@2.5bar310K.n[13] 1.0014928634784568 +gas:Xe@2.5bar310K.n[14] 1.0014920030696985 +gas:Xe@2.5bar310K.n[15] 1.0014912652397876 +gas:Xe@2.5bar310K.n[16] 1.001490627725628 +gas:Xe@2.5bar310K.n[17] 1.0014900731057121 +gas:Xe@2.5bar310K.n[18] 1.0014895875877585 +gas:Xe@2.5bar310K.n[19] 1.0014891601367317 +gas:Xe@2.5bar310K.n[1] 1.0015549115872322 +gas:Xe@2.5bar310K.n[20] 1.001488781838129 +gas:Xe@2.5bar310K.n[21] 1.0014884454266137 +gas:Xe@2.5bar310K.n[22] 1.0014881449326314 +gas:Xe@2.5bar310K.n[23] 1.001487875414401 +gas:Xe@2.5bar310K.n[2] 1.0015369978753232 +gas:Xe@2.5bar310K.n[3] 1.001525179714439 +gas:Xe@2.5bar310K.n[4] 1.0015169567707738 +gas:Xe@2.5bar310K.n[5] 1.001510997534104 +gas:Xe@2.5bar310K.n[6] 1.001506537240993 +gas:Xe@2.5bar310K.n[7] 1.0015031101205536 +gas:Xe@2.5bar310K.n[8] 1.0015004188638372 +gas:Xe@2.5bar310K.n[9] 1.0014982662381902 +gas:Xe@2.5bar310K.sigma 0 +gasdata:Ar 15.76 1 1 1E-23 0 0 0 0 0 0 +gasdata:CO2 13.777 1 1 1.4E-23 0.39021 1.33E-07 2.1 1 0 5E-11 +gasdata:H2 15.426 0 1 6.9E-24 59.322 0.0471 0.3 1 3 3E-10 +gasdata:He 24.587 0 1 3.5E-25 0 0 0 0 0 0 +gasdata:Kr 13.999 1 1 2.8E-23 0 0 0 0 0 0 +gasdata:N2 15.581 0 0.9 8E-24 1.98957 5.76E-06 0.7 6 3 1E-10 +gasdata:Ne 21.565 1 1 7.5E-25 0 0 0 0 0 0 +gasdata:O2 12.07 1 0.53 1.03E-23 1.43768 4.85E-06 1.14 0 1 8E-11 +gasdata:Xe 12.13 1 1 8E-23 0 0 0 0 0 0 +gasindex:Air 0.0001492644 1.936E-05 0.0004180757 0.007434 +gasindex:Ar 0.0002033229 0.00020612 0.0003445831 0.008066 +gasindex:He 4.97777E-05 2.854E-05 1.85694E-05 0.00776 +gasindex:Kr 0.0002610288 2.01E-06 0.0005694682 0.010043 +gasindex:N2 0.0003920995 0.00114624 0.0001880648 0.013476 +gasindex:Ne 9.15448E-05 0.00065697 4.01863E-05 0.005728 +gasindex:Xe 0.0010370161 0.01275 0.0003122861 0.000561 +gasn2:Air 8.5E-24 +gasn2:Ar 1E-23 +gasn2:CO2 1.4E-23 +gasn2:H2 6.9E-24 +gasn2:He 3.5E-25 +gasn2:Kr 2.8E-23 +gasn2:N2 8E-24 +gasn2:Ne 7.5E-25 +gasn2:O2 1.03E-23 +gasn2:Xe 8E-23 +ion:CaF2 10 0.5 2.4E+28 1E-15 1.5E-13 +ion:FusedSilica 9 0.635 2.1E+28 1E-15 1.5E-13 +ion:MgF2 10.8 0.5 2E+28 1E-15 1.5E-13 +ion:MgF2-e 10.8 0.5 2E+28 1E-15 1.5E-13 +ion:N-BK7 4.2 0.5 2E+28 1E-15 1.5E-13 +ion:Quartz 9 0.635 2.1E+28 1E-15 1.5E-13 +ion:Quartz-e 9 0.635 2.1E+28 1E-15 1.5E-13 +ion:Sapphire 8.8 0.5 2.3E+28 1E-15 1.5E-13 +ion:Sapphire-e 8.8 0.5 2.3E+28 1E-15 1.5E-13 +ion:YAG 6.5 0.5 2E+28 1E-15 1.5E-13 +mat:Air.fR 0 +mat:Air.indexReference vacuum +mat:Air.isGas True +mat:Air.n2 8.020626385809312E-24 +mat:Air.n[0] 1.000277924845514 +mat:Air.n[10] 1.000269210549853 +mat:Air.n[11] 1.0002690640610934 +mat:Air.n[12] 1.0002689418618953 +mat:Air.n[13] 1.0002688388564902 +mat:Air.n[14] 1.0002687512204185 +mat:Air.n[15] 1.0002686760376023 +mat:Air.n[16] 1.0002686110529222 +mat:Air.n[17] 1.0002685545000765 +mat:Air.n[18] 1.0002685049796103 +mat:Air.n[19] 1.0002684613710637 +mat:Air.n[1] 1.0002750591031877 +mat:Air.n[20] 1.000268422768739 +mat:Air.n[21] 1.0002683884340942 +mat:Air.n[22] 1.000268357760018 +mat:Air.n[23] 1.0002683302437119 +mat:Air.n[2] 1.0002732826114233 +mat:Air.n[3] 1.0002721022822072 +mat:Air.n[4] 1.000271276971723 +mat:Air.n[5] 1.0002706767334097 +mat:Air.n[6] 1.0002702262835537 +mat:Air.n[7] 1.0002698794731482 +mat:Air.n[8] 1.0002696066974197 +mat:Air.n[9] 1.0002693882398754 +mat:Air.rot[0] 2248554931241.2383 1.569265699801181E-12 10132500000 +mat:Air.rot[10] 17225944739207.072 2.933667689254961E-11 10132500000 +mat:Air.rot[11] 18721249017470.758 1.3735254976574418E-11 10132500000 +mat:Air.rot[12] 20215902305752.38 2.4906297199880417E-11 10132500000 +mat:Air.rot[13] 21709852524853.36 1.0952591090575317E-11 10132500000 +mat:Air.rot[14] 23203047595575.168 1.871658702628809E-11 10132500000 +mat:Air.rot[15] 24695435438719.223 7.777578330763874E-12 10132500000 +mat:Air.rot[16] 26186963975086.938 1.258704170988671E-11 10132500000 +mat:Air.rot[17] 27677581125479.77 4.962568879538937E-12 10132500000 +mat:Air.rot[18] 29167234810699.152 7.631654177290243E-12 10132500000 +mat:Air.rot[19] 30655872951546.543 2.86286446719767E-12 10132500000 +mat:Air.rot[1] 3747504753404.4556 2.2642563876407974E-12 10132500000 +mat:Air.rot[20] 32143443468823.34 4.193698653498015E-12 10132500000 +mat:Air.rot[21] 33629894283330.984 1.49997052480621E-12 10132500000 +mat:Air.rot[22] 35115173315870.902 2.0967545542156054E-12 10132500000 +mat:Air.rot[23] 36599228487244.516 7.161794691698468E-13 10132500000 +mat:Air.rot[24] 38082007718253.41 9.566620672777597E-13 10132500000 +mat:Air.rot[25] 39563458929698.9 3.1243262681170675E-13 10132500000 +mat:Air.rot[26] 41043530042382.28 3.9924720044389376E-13 10132500000 +mat:Air.rot[27] 42522168977105.15 1.2479329359300277E-13 10132500000 +mat:Air.rot[28] 43999323654669.02 1.5269040330250805E-13 10132500000 +mat:Air.rot[29] 45474941995875.24 4.5715448650481953E-14 10132500000 +mat:Air.rot[2] 5246324377571.26 8.544626413377539E-12 10132500000 +mat:Air.rot[30] 46948971921525.13 5.3596683982266644E-14 10132500000 +mat:Air.rot[31] 48421361352420.23 1.5380970712636607E-14 10132500000 +mat:Air.rot[32] 49892058209362.02 1.728950595216933E-14 10132500000 +mat:Air.rot[33] 51361010413151.92 4.758523091662193E-15 10132500000 +mat:Air.rot[34] 52828165884591.31 5.131287801035861E-15 10132500000 +mat:Air.rot[35] 54293472544481.67 1.3551133371226567E-15 10132500000 +mat:Air.rot[36] 55756878313624.31 1.4024498181007775E-15 10132500000 +mat:Air.rot[37] 57218331112820.71 3.5553821587602854E-16 10132500000 +mat:Air.rot[38] 58677778862872.484 3.5329287718330636E-16 10132500000 +mat:Air.rot[39] 60135169484580.92 8.601088873654728E-17 10132500000 +mat:Air.rot[3] 6744961724543.089 6.5885490128546865E-12 10132500000 +mat:Air.rot[40] 61590450898747.58 8.209191337999672E-17 10132500000 +mat:Air.rot[41] 63043571026173.81 1.9199614846340993E-17 10132500000 +mat:Air.rot[42] 64494477787660.836 1.7606991127343624E-17 10132500000 +mat:Air.rot[43] 65943119104010.45 3.957238982243234E-18 10132500000 +mat:Air.rot[44] 67389442896023.88 3.487921669192836E-18 10132500000 +mat:Air.rot[45] 68833397084502.48 7.535642296057971E-19 10132500000 +mat:Air.rot[46] 70274929590247.914 6.385630202222085E-19 10132500000 +mat:Air.rot[47] 71713988334061.53 1.3265610048356837E-19 10132500000 +mat:Air.rot[48] 73150521236744.78 1.0810354216454309E-19 10132500000 +mat:Air.rot[49] 74584476219099.25 2.159983828016327E-20 10132500000 +mat:Air.rot[4] 8243364715121.375 1.7937444014811E-11 10132500000 +mat:Air.rot[50] 76015801201926.02 1.6931950039472002E-20 10132500000 +mat:Air.rot[51] 77444444106026.45 3.254744546483877E-21 10132500000 +mat:Air.rot[52] 78870352852202.34 2.454863497129735E-21 10132500000 +mat:Air.rot[53] 80293475361255.27 4.540931971224246E-22 10132500000 +mat:Air.rot[54] 81713759553986.44 3.2962198128570104E-22 10132500000 +mat:Air.rot[55] 2707960287006.391 2.573320471379643E-12 12665625000 +mat:Air.rot[56] 4873868076822.109 7.773844904721906E-12 12665625000 +mat:Air.rot[57] 7038986541284.577 1.3986381470040512E-11 12665625000 +mat:Air.rot[58] 9202964869125.686 1.9419423345922424E-11 12665625000 +mat:Air.rot[59] 11365452249077.332 2.271727735801741E-11 12665625000 +mat:Air.rot[5] 9741481270107.557 1.1175863498987869E-11 10132500000 +mat:Air.rot[60] 13526097869871.385 2.3322670476178653E-11 12665625000 +mat:Air.rot[61] 15684550920239.756 2.148896732330622E-11 12665625000 +mat:Air.rot[62] 17840460588914.344 1.8013008178679832E-11 12665625000 +mat:Air.rot[63] 19993476064626.973 1.3860310076409957E-11 12665625000 +mat:Air.rot[64] 22143246536109.605 9.850715682472878E-12 12665625000 +mat:Air.rot[65] 24289421192094.09 6.495617341988604E-12 12665625000 +mat:Air.rot[66] 26431649221312.383 3.987436985796795E-12 12665625000 +mat:Air.rot[67] 28569579812496.28 2.284646174199552E-12 12665625000 +mat:Air.rot[68] 30702862154377.707 1.224314303806301E-12 12665625000 +mat:Air.rot[69] 32831145435688.547 6.146725005643933E-13 12665625000 +mat:Air.rot[6] 11239259310303.064 2.601638490069805E-11 10132500000 +mat:Air.rot[70] 34954078845160.746 2.8951731122583987E-13 12665625000 +mat:Air.rot[71] 37071311571526.16 1.2808375125655052E-13 12665625000 +mat:Air.rot[72] 39182492803516.59 5.327692465939188E-14 12665625000 +mat:Air.rot[73] 41287271729863.99 2.085407606168926E-14 12665625000 +mat:Air.rot[74] 43385297539300.414 7.687531045457775E-15 12665625000 +mat:Air.rot[75] 45476219420557.46 2.6707294094773607E-15 12665625000 +mat:Air.rot[76] 47559686562367.14 8.749747480077883E-16 12665625000 +mat:Air.rot[77] 49635348153461.5 2.7048141576528644E-16 12665625000 +mat:Air.rot[78] 51702853382572.15 7.893902192067286E-17 12665625000 +mat:Air.rot[79] 53761851438431.26 2.1761082711830752E-17 12665625000 +mat:Air.rot[7] 12736646756509.334 1.4319740196349454E-11 10132500000 +mat:Air.rot[80] 55811991509770.43 5.6691048185514146E-18 12665625000 +mat:Air.rot[81] 57852922785321.84 1.396353851670905E-18 12665625000 +mat:Air.rot[82] 59884294453817.09 3.25325536262973E-19 12665625000 +mat:Air.rot[83] 61905755703988.36 7.172516419067341E-20 12665625000 +mat:Air.rot[84] 63916955724567.35 1.4970642240069024E-20 12665625000 +mat:Air.rot[85] 65917543704285.9 2.9594180369446863E-21 12665625000 +mat:Air.rot[86] 67907168831875.95 5.543044864865299E-22 12665625000 +mat:Air.rot[8] 14233591529527.81 3.006267893858895E-11 10132500000 +mat:Air.rot[9] 15730041550159.912 1.5131490620637922E-11 10132500000 +mat:Air.sigma 0 +mat:Al2O3_film.fR 0 +mat:Al2O3_film.indexReference vacuum +mat:Al2O3_film.isGas False +mat:Al2O3_film.n2 0 +mat:Al2O3_film.n[0] 1.6700000000000002 +mat:Al2O3_film.n[10] 1.626611570247934 +mat:Al2O3_film.n[11] 1.6258441084082111 +mat:Al2O3_film.n[12] 1.6252029136316337 +mat:Al2O3_film.n[13] 1.6246617329992425 +mat:Al2O3_film.n[14] 1.6242007981516489 +mat:Al2O3_film.n[15] 1.6238049940546968 +mat:Al2O3_film.n[16] 1.6234626038781164 +mat:Al2O3_film.n[17] 1.6231644317867173 +mat:Al2O3_film.n[18] 1.6229031789809842 +mat:Al2O3_film.n[19] 1.6226729927495074 +mat:Al2O3_film.n[1] 1.656215482118606 +mat:Al2O3_film.n[20] 1.6224691358024692 +mat:Al2O3_film.n[21] 1.6222877405702194 +mat:Al2O3_film.n[22] 1.6221256244021682 +mat:Al2O3_film.n[23] 1.621980149006213 +mat:Al2O3_film.n[2] 1.6474348422496572 +mat:Al2O3_film.n[3] 1.6414995968825585 +mat:Al2O3_film.n[4] 1.6373010380622839 +mat:Al2O3_film.n[5] 1.6342222222222222 +mat:Al2O3_film.n[6] 1.6318976799524094 +mat:Al2O3_film.n[7] 1.6300997348819595 +mat:Al2O3_film.n[8] 1.6286805555555557 +mat:Al2O3_film.n[9] 1.6275407672730702 +mat:Al2O3_film.sigma 0 +mat:Ar.fR 0 +mat:Ar.indexReference vacuum +mat:Ar.isGas True +mat:Ar.n2 9.312638580931265E-24 +mat:Ar.n[0] 1.0002637279179725 +mat:Ar.n[10] 1.0002561825917031 +mat:Ar.n[11] 1.0002560550234236 +mat:Ar.n[12] 1.000255948593365 +mat:Ar.n[13] 1.0002558588704076 +mat:Ar.n[14] 1.0002557825279135 +mat:Ar.n[15] 1.0002557170287472 +mat:Ar.n[16] 1.000255660410427 +mat:Ar.n[17] 1.0002556111355816 +mat:Ar.n[18] 1.0002555679859542 +mat:Ar.n[19] 1.0002555299860303 +mat:Ar.n[1] 1.0002612575267105 +mat:Ar.n[20] 1.0002554963471844 +mat:Ar.n[21] 1.0002554664262762 +mat:Ar.n[22] 1.0002554396945715 +mat:Ar.n[23] 1.0002554157141479 +mat:Ar.n[2] 1.0002597202327768 +mat:Ar.n[3] 1.0002586966476648 +mat:Ar.n[4] 1.0002579799943738 +mat:Ar.n[5] 1.0002574583275787 +mat:Ar.n[6] 1.0002570666041257 +mat:Ar.n[7] 1.0002567648751697 +mat:Ar.n[8] 1.0002565274784607 +mat:Ar.n[9] 1.0002563373061768 +mat:Ar.sigma 0 +mat:BaF2.fR 0 +mat:BaF2.indexReference air +mat:BaF2.isGas False +mat:BaF2.n2 2E-20 +mat:BaF2.n[0] 1.4853180797497596 +mat:BaF2.n[10] 1.4684268553294415 +mat:BaF2.n[11] 1.4680572991603098 +mat:BaF2.n[12] 1.467727007764395 +mat:BaF2.n[13] 1.4674264788426412 +mat:BaF2.n[14] 1.4671485806987392 +mat:BaF2.n[15] 1.4668878750976444 +mat:BaF2.n[16] 1.4666401557287354 +mat:BaF2.n[17] 1.466402127160195 +mat:BaF2.n[18] 1.4661711774060082 +mat:BaF2.n[19] 1.4659452141332714 +mat:BaF2.n[1] 1.4799344848043763 +mat:BaF2.n[20] 1.465722544925563 +mat:BaF2.n[21] 1.465501788551541 +mat:BaF2.n[22] 1.465281808384645 +mat:BaF2.n[23] 1.4650616618679246 +mat:BaF2.n[2] 1.476576930479507 +mat:BaF2.n[3] 1.4743263281409242 +mat:BaF2.n[4] 1.47273246170189 +mat:BaF2.n[5] 1.471552643172677 +mat:BaF2.n[6] 1.470646290814712 +mat:BaF2.n[7] 1.4699272509311436 +mat:BaF2.n[8] 1.4693402716427373 +mat:BaF2.n[9] 1.4688485655677475 +mat:BaF2.sigma -1.2 +mat:CO2.fR 0 +mat:CO2.indexReference vacuum +mat:CO2.isGas True +mat:CO2.n2 1.303769401330377E-23 +mat:CO2.n[0] 1.00042230203346 +mat:CO2.n[10] 1.0004060670976973 +mat:CO2.n[11] 1.0004055660104942 +mat:CO2.n[12] 1.000405085228883 +mat:CO2.n[13] 1.0004046148719996 +mat:CO2.n[14] 1.0004041470747331 +mat:CO2.n[15] 1.0004036753560206 +mat:CO2.n[16] 1.0004031941793046 +mat:CO2.n[17] 1.0004026986368817 +mat:CO2.n[18] 1.0004021842151236 +mat:CO2.n[19] 1.0004016466126184 +mat:CO2.n[1] 1.000417427262325 +mat:CO2.n[20] 1.0004010815924589 +mat:CO2.n[21] 1.0004004848555628 +mat:CO2.n[22] 1.0003998519253754 +mat:CO2.n[23] 1.0003991780363763 +mat:CO2.n[2] 1.0004143378134511 +mat:CO2.n[3] 1.0004122289027229 +mat:CO2.n[4] 1.0004107013202344 +mat:CO2.n[5] 1.000409537872914 +mat:CO2.n[6] 1.000408611758891 +mat:CO2.n[7] 1.0004078446611389 +mat:CO2.n[8] 1.0004071858968022 +mat:CO2.n[9] 1.0004066013226531 +mat:CO2.rot[0] 441010905225.598 1.0574632900169905E-12 20000000000 +mat:CO2.rot[10] 6319174149222.688 1.0131907253067312E-10 20000000000 +mat:CO2.rot[11] 6906580413224.388 1.0191232325680581E-10 20000000000 +mat:CO2.rot[12] 7493873640048.917 9.949450240804454E-11 20000000000 +mat:CO2.rot[13] 8081044209511.016 9.448582497307965E-11 20000000000 +mat:CO2.rot[14] 8668082501425.381 8.743508111364913E-11 20000000000 +mat:CO2.rot[15] 9254978895606.656 7.895211023814881E-11 20000000000 +mat:CO2.rot[16] 9841723771869.648 6.964600111678716E-11 20000000000 +mat:CO2.rot[17] 10428307510029.03 6.007482868162053E-11 20000000000 +mat:CO2.rot[18] 11014720489899.459 5.0710192252731245E-11 20000000000 +mat:CO2.rot[19] 11600953091295.746 4.191734052631387E-11 20000000000 +mat:CO2.rot[1] 1029018430807.9545 6.2245346774726894E-12 20000000000 +mat:CO2.rot[20] 12186995694032.568 3.394960936080712E-11 20000000000 +mat:CO2.rot[21] 12772838677924.52 2.6954469750160678E-11 20000000000 +mat:CO2.rot[22] 13358472422786.488 2.098774812435332E-11 20000000000 +mat:CO2.rot[23] 13943887308433.043 1.6032477772127736E-11 20000000000 +mat:CO2.rot[24] 14529073714678.984 1.2019223665839236E-11 20000000000 +mat:CO2.rot[25] 15114022021338.953 8.845410310248434E-12 20000000000 +mat:CO2.rot[26] 15698722608227.752 6.391993167069346E-12 20000000000 +mat:CO2.rot[27] 16283165855159.932 4.536600262762986E-12 20000000000 +mat:CO2.rot[28] 16867342141950.47 3.1629303702866614E-12 20000000000 +mat:CO2.rot[29] 17451241848413.717 2.166675851281538E-12 20000000000 +mat:CO2.rot[2] 1617009121066.053 1.5034523024427247E-11 20000000000 +mat:CO2.rot[30] 18034855354364.734 1.4585327303200123E-12 20000000000 +mat:CO2.rot[31] 18618173039617.902 9.649892981586936E-13 20000000000 +mat:CO2.rot[32] 19201185283988.254 6.275860864447505E-13 20000000000 +mat:CO2.rot[33] 19783882467290.227 4.0125825392869704E-13 20000000000 +mat:CO2.rot[34] 20366254969338.766 2.5224667209940294E-13 20000000000 +mat:CO2.rot[35] 20948293169948.367 1.5592821759190666E-13 20000000000 +mat:CO2.rot[36] 21529987448933.863 9.479061000041183E-14 20000000000 +mat:CO2.rot[37] 22111328186109.977 5.667449969389677E-14 20000000000 +mat:CO2.rot[38] 22692305761291.258 3.3329555898813606E-14 20000000000 +mat:CO2.rot[39] 23272910554292.543 1.9280843531741725E-14 20000000000 +mat:CO2.rot[3] 2204973355814.601 2.670249481345725E-11 20000000000 +mat:CO2.rot[40] 23853132944928.773 1.0972616787462307E-14 20000000000 +mat:CO2.rot[41] 24432963313014.223 6.143460843181738E-15 20000000000 +mat:CO2.rot[42] 25012392038363.66 3.3842615055553723E-15 20000000000 +mat:CO2.rot[43] 25591409500792.215 1.834384177103789E-15 20000000000 +mat:CO2.rot[44] 26170006080114.258 9.784044261798981E-16 20000000000 +mat:CO2.rot[45] 26748172156144.285 5.135386150954867E-16 20000000000 +mat:CO2.rot[46] 27325898108697.7 2.6526383520113857E-16 20000000000 +mat:CO2.rot[47] 27903174317588.312 1.3485150563998484E-16 20000000000 +mat:CO2.rot[48] 28479991162631.754 6.747268014045225E-17 20000000000 +mat:CO2.rot[49] 29056339023641.95 3.3228843260116914E-17 20000000000 +mat:CO2.rot[4] 2792901514868.3125 4.021191382219064E-11 20000000000 +mat:CO2.rot[50] 29632208280433.96 1.610784576435155E-17 20000000000 +mat:CO2.rot[51] 30207589312822.85 7.68623773754052E-18 20000000000 +mat:CO2.rot[52] 30782472500622.426 3.610461600052317E-18 20000000000 +mat:CO2.rot[53] 31356848223648.438 1.669560070129883E-18 20000000000 +mat:CO2.rot[54] 31930706861714.918 7.600620351802592E-19 20000000000 +mat:CO2.rot[55] 32504038794636.363 3.4065946136465757E-19 20000000000 +mat:CO2.rot[56] 33076834402228.516 1.503254716243138E-19 20000000000 +mat:CO2.rot[57] 33649084064305.074 6.53132428540711E-20 20000000000 +mat:CO2.rot[58] 34220778160681.1 2.794099873172849E-20 20000000000 +mat:CO2.rot[59] 34791907071171.316 1.1769816779317199E-20 20000000000 +mat:CO2.rot[5] 3380783978041.893 5.442369878280872E-11 20000000000 +mat:CO2.rot[60] 35362461175590.44 4.882017590846223E-21 20000000000 +mat:CO2.rot[61] 35932430853753.19 1.9940925009923976E-21 20000000000 +mat:CO2.rot[6] 3968611125150.052 6.819325737546979E-11 20000000000 +mat:CO2.rot[7] 4556373336007.503 8.04807333522539E-11 20000000000 +mat:CO2.rot[8] 5144060990428.952 9.044160059590358E-11 20000000000 +mat:CO2.rot[9] 5731664468229.1045 9.748839613976843E-11 20000000000 +mat:CO2.sigma 0 +mat:CS2.fR 0.9 +mat:CS2.hAnisoFraction 1.5 +mat:CS2.h[0] 0 +mat:CS2.h[10] 406314664605.16595 +mat:CS2.h[11] 420889165241.88275 +mat:CS2.h[12] 432784970022.8361 +mat:CS2.h[13] 442331812727.2469 +mat:CS2.h[14] 449820450170.4397 +mat:CS2.h[15] 455507245718.1196 +mat:CS2.h[16] 459618214167.7087 +mat:CS2.h[17] 462352591288.64307 +mat:CS2.h[18] 463885983876.5307 +mat:CS2.h[19] 464373149612.92487 +mat:CS2.h[1] 72856810300.5807 +mat:CS2.h[20] 463950450230.53064 +mat:CS2.h[21] 462738016372.2774 +mat:CS2.h[22] 460841658021.922 +mat:CS2.h[23] 458354550403.106 +mat:CS2.h[24] 455358721730.7915 +mat:CS2.h[25] 451926366098.801 +mat:CS2.h[26] 448121002051.2612 +mat:CS2.h[27] 443998494971.3111 +mat:CS2.h[28] 439607959289.6954 +mat:CS2.h[29] 434992554635.4975 +mat:CS2.h[2] 136023179574.9566 +mat:CS2.h[30] 430190188391.8443 +mat:CS2.h[31] 425234135654.9846 +mat:CS2.h[32] 420153586302.7851 +mat:CS2.h[33] 414974127738.18677 +mat:CS2.h[34] 409718170866.6766 +mat:CS2.h[35] 404405325978.6073 +mat:CS2.h[36] 399052734423.3434 +mat:CS2.h[37] 393675361270.4635 +mat:CS2.h[38] 388286253542.7855 +mat:CS2.h[39] 382896768067.24316 +mat:CS2.h[3] 190655276555.8687 +mat:CS2.h[40] 377516772514.2177 +mat:CS2.h[41] 372154822776.3563 +mat:CS2.h[42] 366818319467.6485 +mat:CS2.h[43] 361513645996.7696 +mat:CS2.h[44] 356246290380.3406 +mat:CS2.h[45] 351020952707.27344 +mat:CS2.h[46] 345841639940.7924 +mat:CS2.h[47] 340711749546.5372 +mat:CS2.h[48] 335634143260.2487 +mat:CS2.h[49] 330611212154.19525 +mat:CS2.h[4] 237773145072.03024 +mat:CS2.h[50] 325644934025.27606 +mat:CS2.h[51] 320736924007.5427 +mat:CS2.h[52] 315888479205.78735 +mat:CS2.h[53] 311100618053.2359 +mat:CS2.h[54] 306374115013.7651 +mat:CS2.h[55] 301709531176.1537 +mat:CS2.h[56] 297107241223.53845 +mat:CS2.h[57] 292567457204.46094 +mat:CS2.h[58] 288090249481.7854 +mat:CS2.h[59] 283675565191.54517 +mat:CS2.h[5] 278276703355.1532 +mat:CS2.h[60] 279323244504.7508 +mat:CS2.h[61] 275033034950.7538 +mat:CS2.h[62] 270804604030.36707 +mat:CS2.h[63] 266637550320.1239 +mat:CS2.h[64] 262531413245.38556 +mat:CS2.h[65] 258485681679.1206 +mat:CS2.h[66] 254499801504.7466 +mat:CS2.h[67] 250573182265.15546 +mat:CS2.h[68] 246705203005.68848 +mat:CS2.h[69] 242895217406.15945 +mat:CS2.h[6] 312959863313.5853 +mat:CS2.h[70] 239142558285.84225 +mat:CS2.h[71] 235446541555.47385 +mat:CS2.h[72] 231806469681.61642 +mat:CS2.h[73] 228221634721.03915 +mat:CS2.h[74] 224691320975.99826 +mat:CS2.h[75] 221214807315.31082 +mat:CS2.h[76] 217791369200.83682 +mat:CS2.h[77] 214420280454.32407 +mat:CS2.h[78] 211100814795.45706 +mat:CS2.h[79] 207832247178.323 +mat:CS2.h[7] 342522990683.31573 +mat:CS2.h[8] 367583901009.3951 +mat:CS2.h[9] 388687563503.20953 +mat:CS2.hb[0] 0 +mat:CS2.hb[12] 649177455034.2542 +mat:CS2.hb[16] 689427321251.563 +mat:CS2.hb[20] 695925675345.7959 +mat:CS2.hb[24] 683038082596.1873 +mat:CS2.hb[28] 659411938934.5431 +mat:CS2.hb[32] 630230379454.1776 +mat:CS2.hb[36] 598579101635.0151 +mat:CS2.hb[40] 566275158771.3265 +mat:CS2.hb[44] 534369435570.51086 +mat:CS2.hb[48] 503451214890.37305 +mat:CS2.hb[4] 356659717608.04535 +mat:CS2.hb[52] 473832718808.681 +mat:CS2.hb[56] 445660861835.3077 +mat:CS2.hb[60] 418984866757.1262 +mat:CS2.hb[64] 393797119868.07837 +mat:CS2.hb[68] 370057804508.5327 +mat:CS2.hb[72] 347709704522.4246 +mat:CS2.hb[76] 326687053801.25525 +mat:CS2.hb[8] 551375851514.0927 +mat:CS2.indexReference vacuum +mat:CS2.isGas False +mat:CS2.n2 3E-18 +mat:CS2.n[0] 1.6905383319371672 +mat:CS2.n[10] 1.5945985746541305 +mat:CS2.n[11] 1.5931507477076712 +mat:CS2.n[12] 1.5919467492024084 +mat:CS2.n[13] 1.5909345093001106 +mat:CS2.n[14] 1.5900752045409918 +mat:CS2.n[15] 1.5893393966253575 +mat:CS2.n[16] 1.5887044272534225 +mat:CS2.n[17] 1.5881526215967114 +mat:CS2.n[18] 1.587670025041089 +mat:CS2.n[19] 1.5872454992483993 +mat:CS2.n[1] 1.6567081555647236 +mat:CS2.n[20] 1.5868700650476824 +mat:CS2.n[21] 1.5865364178682182 +mat:CS2.n[22] 1.5862385657126872 +mat:CS2.n[23] 1.5859715554279212 +mat:CS2.n[2] 1.6369111054483947 +mat:CS2.n[3] 1.6242217070056622 +mat:CS2.n[4] 1.615559752066032 +mat:CS2.n[5] 1.609365782719951 +mat:CS2.n[6] 1.6047747899522622 +mat:CS2.n[7] 1.6012730402989905 +mat:CS2.n[8] 1.598538721784663 +mat:CS2.n[9] 1.5963614122872567 +mat:CS2.sigma 0 +mat:CaF2.fR 0 +mat:CaF2.indexReference air +mat:CaF2.isGas False +mat:CaF2.n2 1.9E-20 +mat:CaF2.n[0] 1.442254396556762 +mat:CaF2.n[10] 1.428649848522522 +mat:CaF2.n[11] 1.4282604528471172 +mat:CaF2.n[12] 1.4278947962856585 +mat:CaF2.n[13] 1.427545561304658 +mat:CaF2.n[14] 1.4272072493453454 +mat:CaF2.n[15] 1.426875662058208 +mat:CaF2.n[16] 1.426547547381282 +mat:CaF2.n[17] 1.4262203531529982 +mat:CaF2.n[18] 1.4258920524464318 +mat:CaF2.n[19] 1.4255610177020026 +mat:CaF2.n[1] 1.4381225188579034 +mat:CaF2.n[20] 1.4252259286660185 +mat:CaF2.n[21] 1.4248857041356537 +mat:CaF2.n[22] 1.424539450721417 +mat:CaF2.n[23] 1.4241864239422308 +mat:CaF2.n[2] 1.435512621502302 +mat:CaF2.n[3] 1.433736394995538 +mat:CaF2.n[4] 1.4324541654499923 +mat:CaF2.n[5] 1.4314818950664907 +mat:CaF2.n[6] 1.4307125830330616 +mat:CaF2.n[7] 1.430080474068672 +mat:CaF2.n[8] 1.4295433076681618 +mat:CaF2.n[9] 1.4290728976265226 +mat:CaF2.sigma 0 +mat:Calcite-e.fR 0 +mat:Calcite-e.indexReference air +mat:Calcite-e.isGas False +mat:Calcite-e.n2 0 +mat:Calcite-e.n[0] 1.4976045226710706 +mat:Calcite-e.n[10] 1.4798367110896096 +mat:Calcite-e.n[11] 1.4794804503679855 +mat:Calcite-e.n[12] 1.4791680666791938 +mat:Calcite-e.n[13] 1.4788894119225149 +mat:Calcite-e.n[14] 1.4786368609212786 +mat:Calcite-e.n[15] 1.478404589676697 +mat:Calcite-e.n[16] 1.4781880835536045 +mat:Calcite-e.n[17] 1.4779837952392794 +mat:Calcite-e.n[18] 1.4777889024682709 +mat:Calcite-e.n[19] 1.4776011335518242 +mat:Calcite-e.n[1] 1.4918659716686213 +mat:Calcite-e.n[20] 1.4774186398342193 +mat:Calcite-e.n[21] 1.4772399011668311 +mat:Calcite-e.n[22] 1.4770636549655831 +mat:Calcite-e.n[23] 1.4768888423469053 +mat:Calcite-e.n[2] 1.488300257710388 +mat:Calcite-e.n[3] 1.485920288507398 +mat:Calcite-e.n[4] 1.48424377948066 +mat:Calcite-e.n[5] 1.483011194687829 +mat:Calcite-e.n[6] 1.4820723653916323 +mat:Calcite-e.n[7] 1.481335334676874 +mat:Calcite-e.n[8] 1.4807411589297648 +mat:Calcite-e.n[9] 1.480250603563726 +mat:Calcite-e.sigma 0 +mat:Calcite.fR 0 +mat:Calcite.indexReference air +mat:Calcite.isGas False +mat:Calcite.n2 0 +mat:Calcite.n[0] 1.682473857079687 +mat:Calcite.n[10] 1.6421948891010032 +mat:Calcite.n[11] 1.6408757074824842 +mat:Calcite.n[12] 1.6396014429982597 +mat:Calcite.n[13] 1.6383516790439676 +mat:Calcite.n[14] 1.637110990564729 +mat:Calcite.n[15] 1.6358674933384865 +mat:Calcite.n[16] 1.6346118599360115 +mat:Calcite.n[17] 1.6333366378904275 +mat:Calcite.n[18] 1.6320357681583841 +mat:Calcite.n[19] 1.630704239106129 +mat:Calcite.n[1] 1.6701609384309661 +mat:Calcite.n[20] 1.6293378339227185 +mat:Calcite.n[21] 1.6279329435338932 +mat:Calcite.n[22] 1.6264864261451 +mat:Calcite.n[23] 1.6249955004439909 +mat:Calcite.n[2] 1.662576972312201 +mat:Calcite.n[3] 1.65746546524914 +mat:Calcite.n[4] 1.6537713692954616 +mat:Calcite.n[5] 1.6509430634441098 +mat:Calcite.n[6] 1.6486676634913888 +mat:Calcite.n[7] 1.6467562595261298 +mat:Calcite.n[8] 1.6450889779303013 +mat:Calcite.n[9] 1.6435865864115178 +mat:Calcite.sigma 0 +mat:FusedSilica.fR 0.18 +mat:FusedSilica.hAnisoFraction 0.25 +mat:FusedSilica.h[0] 0 +mat:FusedSilica.h[10] 37891675258.60172 +mat:FusedSilica.h[11] -9088365606.445019 +mat:FusedSilica.h[12] -4111573547.5928917 +mat:FusedSilica.h[13] 3638146801.4285297 +mat:FusedSilica.h[14] -671729090.5821713 +mat:FusedSilica.h[15] -479447889.30336934 +mat:FusedSilica.h[16] 342900269.87257797 +mat:FusedSilica.h[17] -44042535.18790392 +mat:FusedSilica.h[18] -53310869.69707302 +mat:FusedSilica.h[19] 31703562.475412227 +mat:FusedSilica.h[1] 38156822899006.04 +mat:FusedSilica.h[20] -2189208.194580782 +mat:FusedSilica.h[21] -5722626.601085337 +mat:FusedSilica.h[22] 2871100.3953604843 +mat:FusedSilica.h[23] -10706.497803657006 +mat:FusedSilica.h[24] -597302.3038872388 +mat:FusedSilica.h[25] 254020.72680886672 +mat:FusedSilica.h[26] 18125.84351552139 +mat:FusedSilica.h[27] -60886.09271795594 +mat:FusedSilica.h[28] 21865.491385953057 +mat:FusedSilica.h[29] 3545.6158091514135 +mat:FusedSilica.h[2] -16083969765245.887 +mat:FusedSilica.h[30] -6077.813785041424 +mat:FusedSilica.h[31] 1818.735467288001 +mat:FusedSilica.h[32] 507.34056232980015 +mat:FusedSilica.h[33] -595.0832644221234 +mat:FusedSilica.h[34] 144.49678825787223 +mat:FusedSilica.h[35] 63.82754177110512 +mat:FusedSilica.h[36] -57.19293457318968 +mat:FusedSilica.h[37] 10.729144760228479 +mat:FusedSilica.h[38] 7.465711508879934 +mat:FusedSilica.h[39] -5.395918327513887 +mat:FusedSilica.h[3] -1218345054339.048 +mat:FusedSilica.h[40] 0.7096043063218415 +mat:FusedSilica.h[41] 0.8319315512460381 +mat:FusedSilica.h[42] -0.4994192367059566 +mat:FusedSilica.h[43] 0.036134254655386416 +mat:FusedSilica.h[44] 0.08945254778356916 +mat:FusedSilica.h[45] -0.045280437160581555 +mat:FusedSilica.h[46] 0.0003364627714771777 +mat:FusedSilica.h[47] 0.009349468523875578 +mat:FusedSilica.h[48] -0.00401154031867023 +mat:FusedSilica.h[49] -0.0002687994706535023 +mat:FusedSilica.h[4] 3884943398143.1646 +mat:FusedSilica.h[50] 0.0009541696288343015 +mat:FusedSilica.h[51] -0.00034586079714891987 +mat:FusedSilica.h[52] -5.4216617481374955E-05 +mat:FusedSilica.h[53] 9.534990034951208E-05 +mat:FusedSilica.h[54] -2.882773392369138E-05 +mat:FusedSilica.h[55] -7.834878558345925E-06 +mat:FusedSilica.h[56] 9.345201025626478E-06 +mat:FusedSilica.h[57] -2.296935279948748E-06 +mat:FusedSilica.h[58] -9.906500235089946E-07 +mat:FusedSilica.h[59] 8.990453343772384E-07 +mat:FusedSilica.h[5] -1382213114005.2488 +mat:FusedSilica.h[60] -1.7131655603613194E-07 +mat:FusedSilica.h[61] -1.1623631003310734E-07 +mat:FusedSilica.h[62] 8.490615258360499E-08 +mat:FusedSilica.h[63] -1.1425422627202045E-08 +mat:FusedSilica.h[64] -1.298122060178259E-08 +mat:FusedSilica.h[65] 7.866778779772512E-09 +mat:FusedSilica.h[66] -5.950147419731122E-10 +mat:FusedSilica.h[67] -1.398154148925782E-09 +mat:FusedSilica.h[68] 7.140756782039504E-10 +mat:FusedSilica.h[69] -7.930128801888032E-12 +mat:FusedSilica.h[6] -231694071155.4879 +mat:FusedSilica.h[70] -1.4633566356302637E-10 +mat:FusedSilica.h[71] 6.334605986955864E-11 +mat:FusedSilica.h[72] 3.971814667428607E-12 +mat:FusedSilica.h[73] -1.4952265808938753E-11 +mat:FusedSilica.h[74] 5.47018295192547E-12 +mat:FusedSilica.h[75] 8.283584951400076E-13 +mat:FusedSilica.h[76] -1.4957846057379467E-12 +mat:FusedSilica.h[77] 4.568739003417348E-13 +mat:FusedSilica.h[78] 1.2095073308792888E-13 +mat:FusedSilica.h[79] -1.4674946030989284E-13 +mat:FusedSilica.h[7] 387391935331.8114 +mat:FusedSilica.h[8] -114728806844.29845 +mat:FusedSilica.h[9] -32840956228.315388 +mat:FusedSilica.hb[0] 0 +mat:FusedSilica.hb[12] -1027893386.8982229 +mat:FusedSilica.hb[16] 85725067.46814449 +mat:FusedSilica.hb[20] -547302.0486451956 +mat:FusedSilica.hb[24] -149325.5759718097 +mat:FusedSilica.hb[28] 5466.372846488264 +mat:FusedSilica.hb[32] 126.83514058245004 +mat:FusedSilica.hb[36] -14.29823364329742 +mat:FusedSilica.hb[40] 0.17740107658046037 +mat:FusedSilica.hb[44] 0.02236313694589229 +mat:FusedSilica.hb[48] -0.0010028850796675575 +mat:FusedSilica.hb[4] 971235849535.7911 +mat:FusedSilica.hb[52] -1.3554154370343739E-05 +mat:FusedSilica.hb[56] 2.3363002564066195E-06 +mat:FusedSilica.hb[60] -4.2829139009032985E-08 +mat:FusedSilica.hb[64] -3.2453051504456476E-09 +mat:FusedSilica.hb[68] 1.785189195509876E-10 +mat:FusedSilica.hb[72] 9.929536668571517E-13 +mat:FusedSilica.hb[76] -3.7394615143448667E-13 +mat:FusedSilica.hb[8] -28682201711.07461 +mat:FusedSilica.indexReference air +mat:FusedSilica.isGas False +mat:FusedSilica.n2 2.6E-20 +mat:FusedSilica.n[0] 1.4705247003545434 +mat:FusedSilica.n[10] 1.4495937506203342 +mat:FusedSilica.n[11] 1.4487822402987154 +mat:FusedSilica.n[12] 1.4479856282387724 +mat:FusedSilica.n[13] 1.4471932747710237 +mat:FusedSilica.n[14] 1.4463970736117726 +mat:FusedSilica.n[15] 1.4455907199008504 +mat:FusedSilica.n[16] 1.4447692110383097 +mat:FusedSilica.n[17] 1.4439284991796102 +mat:FusedSilica.n[18] 1.44306524475408 +mat:FusedSilica.n[19] 1.4421766386342445 +mat:FusedSilica.n[1] 1.4645490092733653 +mat:FusedSilica.n[20] 1.441260271803671 +mat:FusedSilica.n[21] 1.4403140384268256 +mat:FusedSilica.n[22] 1.4393360627562966 +mat:FusedSilica.n[23] 1.4383246432799348 +mat:FusedSilica.n[2] 1.4607426895900246 +mat:FusedSilica.n[3] 1.4581083794127243 +mat:FusedSilica.n[4] 1.4561584856339125 +mat:FusedSilica.n[5] 1.4546303650805776 +mat:FusedSilica.n[6] 1.4533718688811452 +mat:FusedSilica.n[7] 1.4522896843030775 +mat:FusedSilica.n[8] 1.4513238748852377 +mat:FusedSilica.n[9] 1.4504344306088113 +mat:FusedSilica.sigma 0 +mat:H2.fR 0 +mat:H2.indexReference vacuum +mat:H2.isGas True +mat:H2.n2 6.425720620842573E-24 +mat:H2.n[0] 1.0001311079927335 +mat:H2.n[10] 1.0001256122110478 +mat:H2.n[11] 1.0001255196313077 +mat:H2.n[12] 1.000125442400186 +mat:H2.n[13] 1.0001253772982628 +mat:H2.n[14] 1.0001253219091193 +mat:H2.n[15] 1.0001252743901676 +mat:H2.n[16] 1.000125233316389 +mat:H2.n[17] 1.0001251975716043 +mat:H2.n[18] 1.0001251662714268 +mat:H2.n[19] 1.0001251387077603 +mat:H2.n[1] 1.0001293041381338 +mat:H2.n[20] 1.000125114308218 +mat:H2.n[21] 1.0001250926060434 +mat:H2.n[22] 1.0001250732175373 +mat:H2.n[23] 1.000125055824922 +mat:H2.n[2] 1.0001281838931637 +mat:H2.n[3] 1.0001274389235275 +mat:H2.n[4] 1.0001269177710106 +mat:H2.n[5] 1.0001265386323996 +mat:H2.n[6] 1.00012625405417 +mat:H2.n[7] 1.0001260349242411 +mat:H2.n[8] 1.0001258625575369 +mat:H2.n[9] 1.0001257245056607 +mat:H2.rot[0] 66725795046668.76 5.3490131781662795E-11 3333333333.3333335 +mat:H2.rot[1] 110499898500117.75 1.8493397082165407E-10 3333333333.3333335 +mat:H2.rot[2] 153209362087071.5 2.8790300298538214E-11 3333333333.3333335 +mat:H2.rot[3] 194428329860931.75 2.0257511933906277E-11 3333333333.3333335 +mat:H2.rot[4] 233730945875100.5 8.596178054161567E-13 3333333333.3333335 +mat:H2.rot[5] 270691354182979.66 1.867904121495036E-13 3333333333.3333335 +mat:H2.rot[6] 304883698837971 2.6707881683453916E-15 3333333333.3333335 +mat:H2.rot[7] 335882123893476.56 2.119023149408349E-16 3333333333.3333335 +mat:H2.rot[8] 363260773402898.1 1.202632573664938E-18 3333333333.3333335 +mat:H2.rot[9] 386593791419637.7 4.146514729197337E-20 3333333333.3333335 +mat:H2.sigma 0 +mat:He.fR 0 +mat:He.indexReference vacuum +mat:He.isGas True +mat:He.n2 3.259423503325943E-25 +mat:He.n[0] 1.0000322689374752 +mat:He.n[10] 1.0000318804404364 +mat:He.n[11] 1.0000318738619158 +mat:He.n[12] 1.0000318683732052 +mat:He.n[13] 1.0000318637459145 +mat:He.n[14] 1.000031859808564 +mat:He.n[15] 1.0000318564303596 +mat:He.n[16] 1.0000318535101247 +mat:He.n[17] 1.000031850968594 +mat:He.n[18] 1.0000318487429523 +mat:He.n[19] 1.0000318467828986 +mat:He.n[1] 1.0000321418723626 +mat:He.n[20] 1.000031845047766 +mat:He.n[21] 1.0000318435043898 +mat:He.n[22] 1.0000318421255028 +mat:He.n[23] 1.000031840888521 +mat:He.n[2] 1.0000320627365653 +mat:He.n[3] 1.000032010017667 +mat:He.n[4] 1.0000319730939973 +mat:He.n[5] 1.0000319462097487 +mat:He.n[6] 1.0000319260184367 +mat:He.n[7] 1.000031910463694 +mat:He.n[8] 1.0000318982240775 +mat:He.n[9] 1.000031888418395 +mat:He.sigma 0 +mat:HfO2.fR 0 +mat:HfO2.indexReference vacuum +mat:HfO2.isGas False +mat:HfO2.n2 0 +mat:HfO2.n[0] 2.05 +mat:HfO2.n[10] 1.9632231404958678 +mat:HfO2.n[11] 1.9616882168164218 +mat:HfO2.n[12] 1.9604058272632674 +mat:HfO2.n[13] 1.9593234659984848 +mat:HfO2.n[14] 1.9584015963032977 +mat:HfO2.n[15] 1.9576099881093936 +mat:HfO2.n[16] 1.9569252077562327 +mat:HfO2.n[17] 1.9563288635734346 +mat:HfO2.n[18] 1.9558063579619682 +mat:HfO2.n[19] 1.9553459854990143 +mat:HfO2.n[1] 2.0224309642372114 +mat:HfO2.n[20] 1.9549382716049382 +mat:HfO2.n[21] 1.9545754811404386 +mat:HfO2.n[22] 1.9542512488043362 +mat:HfO2.n[23] 1.9539602980124253 +mat:HfO2.n[2] 2.004869684499314 +mat:HfO2.n[3] 1.9929991937651168 +mat:HfO2.n[4] 1.9846020761245675 +mat:HfO2.n[5] 1.9784444444444444 +mat:HfO2.n[6] 1.9737953599048186 +mat:HfO2.n[7] 1.9701994697639187 +mat:HfO2.n[8] 1.9673611111111111 +mat:HfO2.n[9] 1.96508153454614 +mat:HfO2.sigma 0 +mat:Kr.fR 0 +mat:Kr.indexReference vacuum +mat:Kr.isGas True +mat:Kr.n2 2.607538802660754E-23 +mat:Kr.n[0] 1.0004043842929298 +mat:Kr.n[10] 1.0003888497893212 +mat:Kr.n[11] 1.0003885903531156 +mat:Kr.n[12] 1.0003883739865254 +mat:Kr.n[13] 1.0003881916415909 +mat:Kr.n[14] 1.0003880365309745 +mat:Kr.n[15] 1.0003879034816177 +mat:Kr.n[16] 1.0003877884944254 +mat:Kr.n[17] 1.0003876884381175 +mat:Kr.n[18] 1.0003876008324324 +mat:Kr.n[19] 1.0003875236920459 +mat:Kr.n[1] 1.000399256867088 +mat:Kr.n[20] 1.0003874554125045 +mat:Kr.n[21] 1.0003873946857182 +mat:Kr.n[22] 1.0003873404365633 +mat:Kr.n[23] 1.0003872917747731 +mat:Kr.n[2] 1.0003960865346706 +mat:Kr.n[3] 1.000393984237055 +mat:Kr.n[4] 1.0003925164185805 +mat:Kr.n[5] 1.0003914500728102 +mat:Kr.n[6] 1.0003906505119609 +mat:Kr.n[7] 1.0003900353229602 +mat:Kr.n[8] 1.0003895517156274 +mat:Kr.n[9] 1.0003891645744853 +mat:Kr.sigma 0 +mat:MgF2-e.fR 0 +mat:MgF2-e.indexReference air +mat:MgF2-e.isGas False +mat:MgF2-e.n2 9E-21 +mat:MgF2-e.n[0] 1.3963311779392364 +mat:MgF2-e.n[10] 1.3849293221276457 +mat:MgF2-e.n[11] 1.384509430709598 +mat:MgF2-e.n[12] 1.3841010351422132 +mat:MgF2-e.n[13] 1.3836983619024819 +mat:MgF2-e.n[14] 1.3832970630566694 +mat:MgF2-e.n[15] 1.382893809602014 +mat:MgF2-e.n[16] 1.3824860138911124 +mat:MgF2-e.n[17] 1.382071636310662 +mat:MgF2-e.n[18] 1.3816490481819266 +mat:MgF2-e.n[19] 1.3812169329296835 +mat:MgF2-e.n[1] 1.3930632853322482 +mat:MgF2-e.n[20] 1.380774213771639 +mat:MgF2-e.n[21] 1.3803200000895148 +mat:MgF2-e.n[22] 1.379853547157772 +mat:MgF2-e.n[23] 1.379374225554781 +mat:MgF2-e.n[2] 1.3909709751245805 +mat:MgF2-e.n[3] 1.389521739293075 +mat:MgF2-e.n[4] 1.388451571822497 +mat:MgF2-e.n[5] 1.3876169988791893 +mat:MgF2-e.n[6] 1.3869344254556173 +mat:MgF2-e.n[7] 1.3863523964912259 +mat:MgF2-e.n[8] 1.3858377980471859 +mat:MgF2-e.n[9] 1.3853685178736004 +mat:MgF2-e.sigma 0 +mat:MgF2.fR 0 +mat:MgF2.indexReference air +mat:MgF2.isGas False +mat:MgF2.n2 9E-21 +mat:MgF2.n[0] 1.384249999050326 +mat:MgF2.n[10] 1.3733521123213326 +mat:MgF2.n[11] 1.3729557300890631 +mat:MgF2.n[12] 1.3725707616667773 +mat:MgF2.n[13] 1.3721916719867893 +mat:MgF2.n[14] 1.371814293332301 +mat:MgF2.n[15] 1.3714354352608609 +mat:MgF2.n[16] 1.371052618367343 +mat:MgF2.n[17] 1.3706638888716018 +mat:MgF2.n[18] 1.3702676871351362 +mat:MgF2.n[19] 1.3698627528832703 +mat:MgF2.n[1] 1.3811152740349502 +mat:MgF2.n[20] 1.3694480558632347 +mat:MgF2.n[21] 1.3690227444190755 +mat:MgF2.n[22] 1.368586106876804 +mat:MgF2.n[23] 1.3681375422148763 +mat:MgF2.n[2] 1.3791103887685234 +mat:MgF2.n[3] 1.3777233135974853 +mat:MgF2.n[4] 1.3767004258760698 +mat:MgF2.n[5] 1.375903970977896 +mat:MgF2.n[6] 1.3752537156301479 +mat:MgF2.n[7] 1.3747002868297622 +mat:MgF2.n[8] 1.3742119167516604 +mat:MgF2.n[9] 1.3737673953530638 +mat:MgF2.sigma 0 +mat:N-BK7.fR 0 +mat:N-BK7.indexReference air +mat:N-BK7.isGas False +mat:N-BK7.n2 3.4E-20 +mat:N-BK7.n[0] 1.5312739990927977 +mat:N-BK7.n[10] 1.5065725335499067 +mat:N-BK7.n[11] 1.505691037962284 +mat:N-BK7.n[12] 1.5048334644355341 +mat:N-BK7.n[13] 1.5039871269191525 +mat:N-BK7.n[14] 1.5031423935441257 +mat:N-BK7.n[15] 1.5022918043851272 +mat:N-BK7.n[16] 1.5014294705410407 +mat:N-BK7.n[17] 1.5005506562530317 +mat:N-BK7.n[18] 1.499651482822452 +mat:N-BK7.n[19] 1.4987287152286497 +mat:N-BK7.n[1] 1.5240240187707679 +mat:N-BK7.n[20] 1.4977796059272208 +mat:N-BK7.n[21] 1.4968017788385575 +mat:N-BK7.n[22] 1.4957931420087422 +mat:N-BK7.n[23] 1.494751821004361 +mat:N-BK7.n[2] 1.519454121907651 +mat:N-BK7.n[3] 1.5163213240694566 +mat:N-BK7.n[4] 1.514025442825728 +mat:N-BK7.n[5] 1.5122456441038383 +mat:N-BK7.n[6] 1.5107970193085198 +mat:N-BK7.n[7] 1.5095665847321544 +mat:N-BK7.n[8] 1.5084819791401116 +mat:N-BK7.n[9] 1.5074950170896975 +mat:N-BK7.sigma 0 +mat:N-SF11.fR 0 +mat:N-SF11.indexReference air +mat:N-SF11.isGas False +mat:N-SF11.n2 1E-19 +mat:N-SF11.n[0] 1.8459351153020331 +mat:N-SF11.n[10] 1.7533577706460663 +mat:N-SF11.n[11] 1.7515613445874847 +mat:N-SF11.n[12] 1.7499369044948851 +mat:N-SF11.n[13] 1.7484392835112879 +mat:N-SF11.n[14] 1.7470348294257774 +mat:N-SF11.n[15] 1.7456980057756968 +mat:N-SF11.n[16] 1.744409111847868 +mat:N-SF11.n[17] 1.7431527177800399 +mat:N-SF11.n[18] 1.7419165684275673 +mat:N-SF11.n[19] 1.7406908015023619 +mat:N-SF11.n[1] 1.8121910122322635 +mat:N-SF11.n[20] 1.7394673806818965 +mat:N-SF11.n[21] 1.7382396784428715 +mat:N-SF11.n[22] 1.7370021648944045 +mat:N-SF11.n[23] 1.7357501727778784 +mat:N-SF11.n[2] 1.7936266717674108 +mat:N-SF11.n[3] 1.7819825929807904 +mat:N-SF11.n[4] 1.7740501171383285 +mat:N-SF11.n[5] 1.7683133485298976 +mat:N-SF11.n[6] 1.7639644929580058 +mat:N-SF11.n[7] 1.7605357637748633 +mat:N-SF11.n[8] 1.757739026688389 +mat:N-SF11.n[9] 1.7553880650542435 +mat:N-SF11.sigma 0 +mat:N2.fR 0 +mat:N2.indexReference vacuum +mat:N2.isGas True +mat:N2.n2 7.450110864745011E-24 +mat:N2.n[0] 1.0002794751981672 +mat:N2.n[10] 1.000271265603557 +mat:N2.n[11] 1.00027112990679 +mat:N2.n[12] 1.0002710167701654 +mat:N2.n[13] 1.0002709214463235 +mat:N2.n[14] 1.0002708403762504 +mat:N2.n[15] 1.0002707708488223 +mat:N2.n[16] 1.0002707107691082 +mat:N2.n[17] 1.0002706584974115 +mat:N2.n[18] 1.0002706127353702 +mat:N2.n[19] 1.0002705724440106 +mat:N2.n[1] 1.0002767462517559 +mat:N2.n[20] 1.0002705367838904 +mat:N2.n[21] 1.0002705050707743 +mat:N2.n[22] 1.0002704767423982 +mat:N2.n[23] 1.000270451333254 +mat:N2.n[2] 1.0002750686945565 +mat:N2.n[3] 1.0002739602213382 +mat:N2.n[4] 1.0002731880968705 +mat:N2.n[5] 1.0002726280742595 +mat:N2.n[6] 1.00027220865777 +mat:N2.n[7] 1.000271886241188 +mat:N2.n[8] 1.000271632958772 +mat:N2.n[9] 1.0002714303081348 +mat:N2.rot[0] 2248554931241.2383 1.9835359000937137E-12 10000000000 +mat:N2.rot[10] 17225944739207.072 3.708126151817012E-11 10000000000 +mat:N2.rot[11] 18721249017470.758 1.736122273393724E-11 10000000000 +mat:N2.rot[12] 20215902305752.38 3.148130660131386E-11 10000000000 +mat:N2.rot[13] 21709852524853.36 1.3843963855168148E-11 10000000000 +mat:N2.rot[14] 23203047595575.168 2.3657575832170575E-11 10000000000 +mat:N2.rot[15] 24695435438719.223 9.830779986343695E-12 10000000000 +mat:N2.rot[16] 26186963975086.938 1.5909892831214267E-11 10000000000 +mat:N2.rot[17] 27677581125479.77 6.272636641774835E-12 10000000000 +mat:N2.rot[18] 29167234810699.152 9.646333339008954E-12 10000000000 +mat:N2.rot[19] 30655872951546.543 3.6186315985296134E-12 10000000000 +mat:N2.rot[1] 3747504753404.4556 2.8619970681007358E-12 10000000000 +mat:N2.rot[20] 32143443468823.34 5.3007924881311545E-12 10000000000 +mat:N2.rot[21] 33629894283330.984 1.8959475029706414E-12 10000000000 +mat:N2.rot[22] 35115173315870.902 2.650276452546289E-12 10000000000 +mat:N2.rot[23] 36599228487244.516 9.052435723207548E-13 10000000000 +mat:N2.rot[24] 38082007718253.41 1.2092111329163177E-12 10000000000 +mat:N2.rot[25] 39563458929698.9 3.949116658320627E-13 10000000000 +mat:N2.rot[26] 41043530042382.28 5.046444048274979E-13 10000000000 +mat:N2.rot[27] 42522168977105.15 1.5773745514479607E-13 10000000000 +mat:N2.rot[28] 43999323654669.02 1.9299911837025708E-13 10000000000 +mat:N2.rot[29] 45474941995875.24 5.778386260440803E-14 10000000000 +mat:N2.rot[2] 5246324377571.26 1.080032096920913E-11 10000000000 +mat:N2.rot[30] 46948971921525.13 6.774566398683965E-14 10000000000 +mat:N2.rot[31] 48421361352420.23 1.944139070309764E-14 10000000000 +mat:N2.rot[32] 49892058209362.02 2.185375985427882E-14 10000000000 +mat:N2.rot[33] 51361010413151.92 6.014724839096866E-15 10000000000 +mat:N2.rot[34] 52828165884591.31 6.485895644285782E-15 10000000000 +mat:N2.rot[35] 54293472544481.67 1.7128494895537013E-15 10000000000 +mat:N2.rot[36] 55756878313624.31 1.7726823205498173E-15 10000000000 +mat:N2.rot[37] 57218331112820.71 4.493966924369276E-16 10000000000 +mat:N2.rot[38] 58677778862872.484 4.465586071429916E-16 10000000000 +mat:N2.rot[39] 60135169484580.92 1.0871688945315157E-16 10000000000 +mat:N2.rot[3] 6744961724543.089 8.327859009586461E-12 10000000000 +mat:N2.rot[40] 61590450898747.58 1.0376334442104806E-16 10000000000 +mat:N2.rot[41] 63043571026173.81 2.4268118088934568E-17 10000000000 +mat:N2.rot[42] 64494477787660.836 2.2255057889904997E-17 10000000000 +mat:N2.rot[43] 65943119104010.45 5.001909866203179E-18 10000000000 +mat:N2.rot[44] 67389442896023.88 4.408697550985351E-18 10000000000 +mat:N2.rot[45] 68833397084502.48 9.52497529665585E-19 10000000000 +mat:N2.rot[46] 70274929590247.914 8.0713716946944855E-19 10000000000 +mat:N2.rot[47] 71713988334061.53 1.6767596316476812E-19 10000000000 +mat:N2.rot[48] 73150521236744.78 1.3664177891470684E-19 10000000000 +mat:N2.rot[49] 74584476219099.25 2.730197612192152E-20 10000000000 +mat:N2.rot[4] 8243364715121.375 2.2672746982111764E-11 10000000000 +mat:N2.rot[50] 76015801201926.02 2.140181281356049E-20 10000000000 +mat:N2.rot[51] 77444444106026.45 4.1139640370671055E-21 10000000000 +mat:N2.rot[52] 78870352852202.34 3.1029225178396174E-21 10000000000 +mat:N2.rot[53] 80293475361255.27 5.73969187368829E-22 10000000000 +mat:N2.rot[54] 81713759553986.44 4.1663883523551297E-22 10000000000 +mat:N2.rot[5] 9741481270107.557 1.4126177910852149E-11 10000000000 +mat:N2.rot[6] 11239259310303.064 3.2884446176150194E-11 10000000000 +mat:N2.rot[7] 12736646756509.334 1.810000611309657E-11 10000000000 +mat:N2.rot[8] 14233591529527.81 3.7998920727852055E-11 10000000000 +mat:N2.rot[9] 15730041550159.912 1.9126050401642785E-11 10000000000 +mat:N2.sigma 0 +mat:Nb2O5.fR 0 +mat:Nb2O5.indexReference vacuum +mat:Nb2O5.isGas False +mat:Nb2O5.n2 0 +mat:Nb2O5.n[0] 2.43 +mat:Nb2O5.n[10] 2.2130578512396695 +mat:Nb2O5.n[11] 2.209220542041055 +mat:Nb2O5.n[12] 2.2060145681581687 +mat:Nb2O5.n[13] 2.2033086649962126 +mat:Nb2O5.n[14] 2.2010039907582444 +mat:Nb2O5.n[15] 2.1990249702734843 +mat:Nb2O5.n[16] 2.197313019390582 +mat:Nb2O5.n[17] 2.1958221589335865 +mat:Nb2O5.n[18] 2.194515894904921 +mat:Nb2O5.n[19] 2.193364963747536 +mat:Nb2O5.n[1] 2.361077410593029 +mat:Nb2O5.n[20] 2.192345679012346 +mat:Nb2O5.n[21] 2.1914387028510967 +mat:Nb2O5.n[22] 2.190628122010841 +mat:Nb2O5.n[23] 2.1899007450310637 +mat:Nb2O5.n[2] 2.3171742112482856 +mat:Nb2O5.n[3] 2.2874979844127923 +mat:Nb2O5.n[4] 2.2665051903114186 +mat:Nb2O5.n[5] 2.2511111111111113 +mat:Nb2O5.n[6] 2.2394883997620467 +mat:Nb2O5.n[7] 2.2304986744097968 +mat:Nb2O5.n[8] 2.223402777777778 +mat:Nb2O5.n[9] 2.2177038363653505 +mat:Nb2O5.sigma 0 +mat:Ne.fR 0 +mat:Ne.indexReference vacuum +mat:Ne.isGas True +mat:Ne.n2 6.984478935698449E-25 +mat:Ne.n[0] 1.000062206781751 +mat:Ne.n[10] 1.0000614484766097 +mat:Ne.n[11] 1.0000614354135247 +mat:Ne.n[12] 1.0000614245086847 +mat:Ne.n[13] 1.0000614153111833 +mat:Ne.n[14] 1.0000614074820877 +mat:Ne.n[15] 1.0000614007626345 +mat:Ne.n[16] 1.0000613949524912 +mat:Ne.n[17] 1.0000613898946014 +mat:Ne.n[18] 1.0000613854644267 +mat:Ne.n[19] 1.0000613815621855 +mat:Ne.n[1] 1.0000619615622393 +mat:Ne.n[20] 1.0000613781071717 +mat:Ne.n[21] 1.000061375033538 +mat:Ne.n[22] 1.0000613722871294 +mat:Ne.n[23] 1.000061369823076 +mat:Ne.n[2] 1.000061807480771 +mat:Ne.n[3] 1.000061704246229 +mat:Ne.n[4] 1.0000616316591056 +mat:Ne.n[5] 1.0000615786605072 +mat:Ne.n[6] 1.0000615387738687 +mat:Ne.n[7] 1.00006150799818 +mat:Ne.n[8] 1.0000614837519666 +mat:Ne.n[9] 1.0000614643084087 +mat:Ne.sigma 0 +mat:O2.fR 0 +mat:O2.indexReference vacuum +mat:O2.isGas True +mat:O2.n2 9.592017738359203E-24 +mat:O2.n[0] 1.000255172286224 +mat:O2.n[10] 1.0002450945792944 +mat:O2.n[11] 1.0002449294996207 +mat:O2.n[12] 1.0002447919046547 +mat:O2.n[13] 1.0002446760012895 +mat:O2.n[14] 1.000244577449125 +mat:O2.n[15] 1.0002444929434822 +mat:O2.n[16] 1.0002444199318872 +mat:O2.n[17] 1.0002443564172743 +mat:O2.n[18] 1.0002443008188204 +mat:O2.n[19] 1.0002442518718806 +mat:O2.n[1] 1.00025180342595 +mat:O2.n[20] 1.000244208554936 +mat:O2.n[21] 1.000244170035524 +mat:O2.n[22] 1.0002441356297098 +mat:O2.n[23] 1.000244104771352 +mat:O2.n[2] 1.0002497415887348 +mat:O2.n[3] 1.0002483831541746 +mat:O2.n[4] 1.000247438826021 +mat:O2.n[5] 1.0002467549035645 +mat:O2.n[6] 1.0002462432526615 +mat:O2.n[7] 1.0002458502608447 +mat:O2.n[8] 1.0002455417376044 +mat:O2.n[9] 1.0002452950172183 +mat:O2.rot[0] 2707960287006.391 1.2122529097475831E-11 12500000000 +mat:O2.rot[10] 24289421192094.09 3.05998848997259E-11 12500000000 +mat:O2.rot[11] 26431649221312.383 1.8784221173493194E-11 12500000000 +mat:O2.rot[12] 28569579812496.28 1.0762627520435618E-11 12500000000 +mat:O2.rot[13] 30702862154377.707 5.767562158470908E-12 12500000000 +mat:O2.rot[14] 32831145435688.547 2.8956305117780964E-12 12500000000 +mat:O2.rot[15] 34954078845160.746 1.3638728905290806E-12 12500000000 +mat:O2.rot[16] 37071311571526.16 6.033834568179285E-13 12500000000 +mat:O2.rot[17] 39182492803516.59 2.5097964928606165E-13 12500000000 +mat:O2.rot[18] 41287271729863.99 9.824044329902894E-14 12500000000 +mat:O2.rot[19] 43385297539300.414 3.621481266044845E-14 12500000000 +mat:O2.rot[1] 4873868076822.109 3.662142438334975E-11 12500000000 +mat:O2.rot[20] 45476219420557.46 1.2581408082653576E-14 12500000000 +mat:O2.rot[21] 47559686562367.14 4.121875592352584E-15 12500000000 +mat:O2.rot[22] 49635348153461.5 1.2741976249787517E-15 12500000000 +mat:O2.rot[23] 51702853382572.15 3.718699636530663E-16 12500000000 +mat:O2.rot[24] 53761851438431.26 1.0251321640686086E-16 12500000000 +mat:O2.rot[25] 55811991509770.43 2.6706307622340096E-17 12500000000 +mat:O2.rot[26] 57852922785321.84 6.578014819964373E-18 12500000000 +mat:O2.rot[27] 59884294453817.09 1.5325601002138038E-18 12500000000 +mat:O2.rot[28] 61905755703988.36 3.3788655536420895E-19 12500000000 +mat:O2.rot[29] 63916955724567.35 7.0524463696447E-20 12500000000 +mat:O2.rot[2] 7038986541284.577 6.58877579472491E-11 12500000000 +mat:O2.rot[30] 65917543704285.9 1.394137716754052E-20 12500000000 +mat:O2.rot[31] 67907168831875.95 2.611245797415885E-21 12500000000 +mat:O2.rot[3] 9202964869125.686 9.148200824009049E-11 12500000000 +mat:O2.rot[4] 11365452249077.332 1.070177068308747E-10 12500000000 +mat:O2.rot[5] 13526097869871.385 1.0986962355556724E-10 12500000000 +mat:O2.rot[6] 15684550920239.756 1.0123132137981397E-10 12500000000 +mat:O2.rot[7] 17840460588914.344 8.485659606245801E-11 12500000000 +mat:O2.rot[8] 19993476064626.973 6.529385440719513E-11 12500000000 +mat:O2.rot[9] 22143246536109.605 4.640525298728777E-11 12500000000 +mat:O2.sigma 0 +mat:Quartz-e.fR 0 +mat:Quartz-e.indexReference air +mat:Quartz-e.isGas False +mat:Quartz-e.n2 3E-20 +mat:Quartz-e.n[0] 1.5677292532168325 +mat:Quartz-e.n[10] 1.5427413681840016 +mat:Quartz-e.n[11] 1.5417757701076753 +mat:Quartz-e.n[12] 1.5408284268173467 +mat:Quartz-e.n[13] 1.5398866103424467 +mat:Quartz-e.n[14] 1.5389406250633049 +mat:Quartz-e.n[15] 1.5379829320184613 +mat:Quartz-e.n[16] 1.5370075516422843 +mat:Quartz-e.n[17] 1.5360096479232184 +mat:Quartz-e.n[18] 1.5349852334301084 +mat:Quartz-e.n[19] 1.5339309564863233 +mat:Quartz-e.n[1] 1.5605977096038552 +mat:Quartz-e.n[20] 1.5328439451875753 +mat:Quartz-e.n[21] 1.5317216913978786 +mat:Quartz-e.n[22] 1.5305619632788812 +mat:Quartz-e.n[23] 1.529362738457574 +mat:Quartz-e.n[2] 1.556050468185269 +mat:Quartz-e.n[3] 1.5529023887981297 +mat:Quartz-e.n[4] 1.5505724208807832 +mat:Quartz-e.n[5] 1.5487470885001702 +mat:Quartz-e.n[6] 1.5472446244606117 +mat:Quartz-e.n[7] 1.5459534845780083 +mat:Quartz-e.n[8] 1.5448019940920534 +mat:Quartz-e.n[9] 1.5437422951597592 +mat:Quartz-e.sigma 0 +mat:Quartz.fR 0 +mat:Quartz.indexReference air +mat:Quartz.isGas False +mat:Quartz.n2 3E-20 +mat:Quartz.n[0] 1.5581636973502846 +mat:Quartz.n[10] 1.5340221676233328 +mat:Quartz.n[11] 1.5330933882737892 +mat:Quartz.n[12] 1.5321826983907345 +mat:Quartz.n[13] 1.5312777822936867 +mat:Quartz.n[14] 1.5303692599824148 +mat:Quartz.n[15] 1.5294498396342282 +mat:Quartz.n[16] 1.5285137395325783 +mat:Quartz.n[17] 1.527556285568654 +mat:Quartz.n[18] 1.526573625724524 +mat:Quartz.n[19] 1.5255625240683228 +mat:Quartz.n[1] 1.5512682632920014 +mat:Quartz.n[20] 1.5245202097729824 +mat:Quartz.n[21] 1.5234442648355124 +mat:Quartz.n[22] 1.522332539419674 +mat:Quartz.n[23] 1.5211830871802845 +mat:Quartz.n[2] 1.5468710875132412 +mat:Quartz.n[3] 1.543827353217716 +mat:Quartz.n[4] 1.5415754164926858 +mat:Quartz.n[5] 1.53981214226665 +mat:Quartz.n[6] 1.5383617080114953 +mat:Quartz.n[7] 1.5371161955077859 +mat:Quartz.n[8] 1.5360062501661682 +mat:Quartz.n[9] 1.5349855591977417 +mat:Quartz.sigma 0 +mat:Sapphire-e.fR 0 +mat:Sapphire-e.indexReference air +mat:Sapphire-e.isGas False +mat:Sapphire-e.n2 3E-20 +mat:Sapphire-e.n[0] 1.7785598222657897 +mat:Sapphire-e.n[10] 1.7464567294731406 +mat:Sapphire-e.n[11] 1.7452488120089993 +mat:Sapphire-e.n[12] 1.744069244145825 +mat:Sapphire-e.n[13] 1.7429019595054132 +mat:Sapphire-e.n[14] 1.74173484343037 +mat:Sapphire-e.n[15] 1.7405586013629477 +mat:Sapphire-e.n[16] 1.7393659873576057 +mat:Sapphire-e.n[17] 1.7381512673031414 +mat:Sapphire-e.n[18] 1.7369098385608586 +mat:Sapphire-e.n[19] 1.7356379559517419 +mat:Sapphire-e.n[1] 1.7693435428649147 +mat:Sapphire-e.n[20] 1.734332531372706 +mat:Sapphire-e.n[21] 1.732990985234108 +mat:Sapphire-e.n[22] 1.731611134921267 +mat:Sapphire-e.n[23] 1.7301911100740897 +mat:Sapphire-e.n[2] 1.7634742320712058 +mat:Sapphire-e.n[3] 1.7594169599418006 +mat:Sapphire-e.n[4] 1.7564201879420875 +mat:Sapphire-e.n[5] 1.7540787202037296 +mat:Sapphire-e.n[6] 1.7521577295562618 +mat:Sapphire-e.n[7] 1.7505132132019658 +mat:Sapphire-e.n[8] 1.7490527431527623 +mat:Sapphire-e.n[9] 1.7477147128143995 +mat:Sapphire-e.sigma 0 +mat:Sapphire.fR 0 +mat:Sapphire.indexReference air +mat:Sapphire.isGas False +mat:Sapphire.n2 3E-20 +mat:Sapphire.n[0] 1.787017723627321 +mat:Sapphire.n[10] 1.7543125615473458 +mat:Sapphire.n[11] 1.7530936946550886 +mat:Sapphire.n[12] 1.7519046007755767 +mat:Sapphire.n[13] 1.750728838009113 +mat:Sapphire.n[14] 1.7495540010596096 +mat:Sapphire.n[15] 1.7483705647001027 +mat:Sapphire.n[16] 1.7471710953184898 +mat:Sapphire.n[17] 1.7459497023396247 +mat:Sapphire.n[18] 1.744701649499286 +mat:Sapphire.n[19] 1.743423074798408 +mat:Sapphire.n[1] 1.7776026938634977 +mat:Sapphire.n[20] 1.7421107856971136 +mat:Sapphire.n[21] 1.7407621072612698 +mat:Sapphire.n[22] 1.7393747681394267 +mat:Sapphire.n[23] 1.7379468139404122 +mat:Sapphire.n[2] 1.7716109256567716 +mat:Sapphire.n[3] 1.7674726356654984 +mat:Sapphire.n[4] 1.7644193427996395 +mat:Sapphire.n[5] 1.7620367734356606 +mat:Sapphire.n[6] 1.7600848630176138 +mat:Sapphire.n[7] 1.7584164050416082 +mat:Sapphire.n[8] 1.7569369158450179 +mat:Sapphire.n[9] 1.7555834155801766 +mat:Sapphire.sigma 0 +mat:SiO2_film.fR 0 +mat:SiO2_film.indexReference vacuum +mat:SiO2_film.isGas False +mat:SiO2_film.n2 0 +mat:SiO2_film.n[0] 1.4725 +mat:SiO2_film.n[10] 1.4529752066115702 +mat:SiO2_film.n[11] 1.4526298487836948 +mat:SiO2_film.n[12] 1.4523413111342351 +mat:SiO2_film.n[13] 1.452097779849659 +mat:SiO2_film.n[14] 1.451890359168242 +mat:SiO2_film.n[15] 1.4517122473246136 +mat:SiO2_film.n[16] 1.4515581717451522 +mat:SiO2_film.n[17] 1.4514239943040228 +mat:SiO2_film.n[18] 1.451306430541443 +mat:SiO2_film.n[19] 1.4512028467372782 +mat:SiO2_film.n[1] 1.4662969669533725 +mat:SiO2_film.n[20] 1.451111111111111 +mat:SiO2_film.n[21] 1.4510294832565986 +mat:SiO2_film.n[22] 1.4509565309809755 +mat:SiO2_film.n[23] 1.4508910670527957 +mat:SiO2_film.n[2] 1.4623456790123457 +mat:SiO2_film.n[3] 1.4596748185971513 +mat:SiO2_film.n[4] 1.4577854671280277 +mat:SiO2_film.n[5] 1.4564 +mat:SiO2_film.n[6] 1.4553539559785842 +mat:SiO2_film.n[7] 1.4545448806968817 +mat:SiO2_film.n[8] 1.45390625 +mat:SiO2_film.n[9] 1.4533933452728816 +mat:SiO2_film.sigma 0 +mat:TGG.fR 0 +mat:TGG.indexReference air +mat:TGG.isGas False +mat:TGG.n2 0 +mat:TGG.n[0] 2.020886716241975 +mat:TGG.n[10] 1.9434339867520556 +mat:TGG.n[11] 1.9421896445699445 +mat:TGG.n[12] 1.9411530540714113 +mat:TGG.n[13] 1.940280286402814 +mat:TGG.n[14] 1.9395384673555935 +mat:TGG.n[15] 1.938902592203142 +mat:TGG.n[16] 1.938353364140066 +mat:TGG.n[17] 1.9378756962619086 +mat:TGG.n[18] 1.937457653720439 +mat:TGG.n[19] 1.9370896939805025 +mat:TGG.n[1] 1.9946604339776988 +mat:TGG.n[20] 1.9367641127595723 +mat:TGG.n[21] 1.9364746343015133 +mat:TGG.n[22] 1.936216104505247 +mat:TGG.n[23] 1.9359842583889362 +mat:TGG.n[2] 1.978778028775347 +mat:TGG.n[3] 1.968382185039291 +mat:TGG.n[4] 1.9611868146702736 +mat:TGG.n[5] 1.9559915147010005 +mat:TGG.n[6] 1.9521135036541715 +mat:TGG.n[7] 1.949139858893176 +mat:TGG.n[8] 1.9468083911343472 +mat:TGG.n[9] 1.9449458698197166 +mat:TGG.sigma 0 +mat:Ta2O5.fR 0 +mat:Ta2O5.indexReference vacuum +mat:Ta2O5.isGas False +mat:Ta2O5.n2 0 +mat:Ta2O5.n[0] 2.221875 +mat:Ta2O5.n[10] 2.0673485417662727 +mat:Ta2O5.n[11] 2.065250651078407 +mat:Ta2O5.n[12] 2.063514852396426 +mat:Ta2O5.n[13] 2.062061802896035 +mat:Ta2O5.n[14] 2.060832871099951 +mat:Ta2O5.n[15] 2.0597839472571717 +mat:Ta2O5.n[16] 2.0588813151756047 +mat:Ta2O5.n[17] 2.0580988350017826 +mat:Ta2O5.n[18] 2.0574159808561285 +mat:Ta2O5.n[19] 2.0568164485657476 +mat:Ta2O5.n[1] 2.165130476766821 +mat:Ta2O5.n[20] 2.0562871513488794 +mat:Ta2O5.n[21] 2.055817484367735 +mat:Ta2O5.n[22] 2.055398778738528 +mat:Ta2O5.n[23] 2.055023891079659 +mat:Ta2O5.n[2] 2.1326996787978345 +mat:Ta2O5.n[3] 2.112415854696006 +mat:Ta2O5.n[4] 2.0988649561188204 +mat:Ta2O5.n[5] 2.089348148148148 +mat:Ta2O5.n[6] 2.08239835216071 +mat:Ta2O5.n[7] 2.077161924292758 +mat:Ta2O5.n[8] 2.073114239728009 +mat:Ta2O5.n[9] 2.0699181026401736 +mat:Ta2O5.sigma 0 +mat:TiO2.fR 0 +mat:TiO2.indexReference vacuum +mat:TiO2.isGas False +mat:TiO2.n2 0 +mat:TiO2.n[0] 2.6609375 +mat:TiO2.n[10] 2.247503585820641 +mat:TiO2.n[11] 2.24177919545115 +mat:TiO2.n[12] 2.237038952010837 +mat:TiO2.n[13] 2.233068090364615 +mat:TiO2.n[14] 2.229707676594659 +mat:TiO2.n[15] 2.226837989427116 +mat:TiO2.n[16] 2.2243674153628348 +mat:TiO2.n[17] 2.222224857371156 +mat:TiO2.n[18] 2.220354439003436 +mat:TiO2.n[19] 2.2187117418828115 +mat:TiO2.n[1] 2.5104608682411826 +mat:TiO2.n[20] 2.217261088248743 +mat:TiO2.n[21] 2.215973548775725 +mat:TiO2.n[22] 2.214825462097676 +mat:TiO2.n[23] 2.2137973208280313 +mat:TiO2.n[2] 2.4238959734006222 +mat:TiO2.n[3] 2.369476884791614 +mat:TiO2.n[4] 2.3329755390859788 +mat:TiO2.n[5] 2.3072592592592596 +mat:TiO2.n[6] 2.2884319303720306 +mat:TiO2.n[7] 2.274217145033121 +mat:TiO2.n[8] 2.263210946542246 +mat:TiO2.n[9] 2.254508236146104 +mat:TiO2.sigma 0 +mat:Vacuum.fR 0 +mat:Vacuum.indexReference vacuum +mat:Vacuum.isGas False +mat:Vacuum.n2 0 +mat:Vacuum.n[0] 1 +mat:Vacuum.n[10] 1 +mat:Vacuum.n[11] 1 +mat:Vacuum.n[12] 1 +mat:Vacuum.n[13] 1 +mat:Vacuum.n[14] 1 +mat:Vacuum.n[15] 1 +mat:Vacuum.n[16] 1 +mat:Vacuum.n[17] 1 +mat:Vacuum.n[18] 1 +mat:Vacuum.n[19] 1 +mat:Vacuum.n[1] 1 +mat:Vacuum.n[20] 1 +mat:Vacuum.n[21] 1 +mat:Vacuum.n[22] 1 +mat:Vacuum.n[23] 1 +mat:Vacuum.n[2] 1 +mat:Vacuum.n[3] 1 +mat:Vacuum.n[4] 1 +mat:Vacuum.n[5] 1 +mat:Vacuum.n[6] 1 +mat:Vacuum.n[7] 1 +mat:Vacuum.n[8] 1 +mat:Vacuum.n[9] 1 +mat:Vacuum.sigma 0 +mat:Xe.fR 0 +mat:Xe.indexReference vacuum +mat:Xe.isGas True +mat:Xe.n2 7.450110864745012E-23 +mat:Xe.n[0] 1.0006703754092385 +mat:Xe.n[10] 1.0006332873634058 +mat:Xe.n[11] 1.0006326773992977 +mat:Xe.n[12] 1.000632168929076 +mat:Xe.n[13] 1.0006317405750451 +mat:Xe.n[14] 1.0006313763162173 +mat:Xe.n[15] 1.000631063951969 +mat:Xe.n[16] 1.000630794057039 +mat:Xe.n[17] 1.0006305592558977 +mat:Xe.n[18] 1.0006303537094827 +mat:Xe.n[19] 1.0006301727460343 +mat:Xe.n[1] 1.0006580094620368 +mat:Xe.n[20] 1.0006300125915384 +mat:Xe.n[21] 1.0006298701701672 +mat:Xe.n[22] 1.0006297429546736 +mat:Xe.n[23] 1.0006296288529237 +mat:Xe.n[2] 1.0006504253590245 +mat:Xe.n[3] 1.0006454219636716 +mat:Xe.n[4] 1.0006419406774285 +mat:Xe.n[5] 1.0006394177702025 +mat:Xe.n[6] 1.0006375294625867 +mat:Xe.n[7] 1.000636078561765 +mat:Xe.n[8] 1.000634939197165 +mat:Xe.n[9] 1.0006340278675152 +mat:Xe.sigma 0 +mat:YAG.fR 0 +mat:YAG.indexReference air +mat:YAG.isGas False +mat:YAG.n2 6.2E-20 +mat:YAG.n[0] 1.8613433294940562 +mat:YAG.n[10] 1.8144583757262454 +mat:YAG.n[11] 1.8132166944132675 +mat:YAG.n[12] 1.8120632520716644 +mat:YAG.n[13] 1.81097254357576 +mat:YAG.n[14] 1.8099253892615994 +mat:YAG.n[15] 1.8089071242077572 +mat:YAG.n[16] 1.8079063644878237 +mat:YAG.n[17] 1.8069141492113865 +mat:YAG.n[18] 1.8059233328614204 +mat:YAG.n[19] 1.8049281477301218 +mat:YAG.n[1] 1.8467996079786877 +mat:YAG.n[20] 1.8039238840715457 +mat:YAG.n[21] 1.8029066530760562 +mat:YAG.n[22] 1.8018732089997191 +mat:YAG.n[23] 1.8008208141316644 +mat:YAG.n[2] 1.8376958939571475 +mat:YAG.n[3] 1.8315487437002365 +mat:YAG.n[4] 1.8271460276109426 +mat:YAG.n[5] 1.823835854658503 +mat:YAG.n[6] 1.8212411813836888 +mat:YAG.n[7] 1.8191311747867438 +mat:YAG.n[8] 1.8173578886883224 +mat:YAG.n[9] 1.8158228456763355 +mat:YAG.sigma 0 +mat:YVO4-e.fR 0 +mat:YVO4-e.indexReference air +mat:YVO4-e.isGas False +mat:YVO4-e.n2 0 +mat:YVO4-e.n[0] 2.3639479019664744 +mat:YVO4-e.n[10] 2.163760746128542 +mat:YVO4-e.n[11] 2.1606612777228804 +mat:YVO4-e.n[12] 2.157985516444778 +mat:YVO4-e.n[13] 2.1556353132371555 +mat:YVO4-e.n[14] 2.1535379405349455 +mat:YVO4-e.n[15] 2.151638537377356 +mat:YVO4-e.n[16] 2.149895061908157 +mat:YVO4-e.n[17] 2.1482748395593734 +mat:YVO4-e.n[18] 2.1467521532022915 +mat:YVO4-e.n[19] 2.1453065294208846 +mat:YVO4-e.n[1] 2.288885403369704 +mat:YVO4-e.n[20] 2.1439214994528517 +mat:YVO4-e.n[21] 2.142583689799087 +mat:YVO4-e.n[22] 2.141282145637133 +mat:YVO4-e.n[23] 2.140007821142269 +mat:YVO4-e.n[2] 2.247716027440411 +mat:YVO4-e.n[3] 2.2222415526924886 +mat:YVO4-e.n[4] 2.2051827734339 +mat:YVO4-e.n[5] 2.1930900892513234 +mat:YVO4-e.n[6] 2.1841336096776796 +mat:YVO4-e.n[7] 2.1772605956675792 +mat:YVO4-e.n[8] 2.171826967490583 +mat:YVO4-e.n[9] 2.1674191040734425 +mat:YVO4-e.sigma 0 +mat:YVO4.fR 0 +mat:YVO4.indexReference air +mat:YVO4.isGas False +mat:YVO4.n2 0 +mat:YVO4.n[0] 2.0970215687049287 +mat:YVO4.n[10] 1.9563461637309014 +mat:YVO4.n[11] 1.954061298298099 +mat:YVO4.n[12] 1.9520639480873914 +mat:YVO4.n[13] 1.9502856710904688 +mat:YVO4.n[14] 1.948675759128568 +mat:YVO4.n[15] 1.9471959648085537 +mat:YVO4.n[16] 1.9458169794443454 +mat:YVO4.n[17] 1.9445160249573346 +mat:YVO4.n[18] 1.9432751730110736 +mat:YVO4.n[19] 1.9420801498918534 +mat:YVO4.n[1] 2.044459846885524 +mat:YVO4.n[20] 1.9409194725415286 +mat:YVO4.n[21] 1.9397838145436637 +mat:YVO4.n[22] 1.9386655344720218 +mat:YVO4.n[23] 1.9375583206277247 +mat:YVO4.n[2] 2.015636061833214 +mat:YVO4.n[3] 1.9977868421920593 +mat:YVO4.n[4] 1.9858127778482226 +mat:YVO4.n[5] 1.977299820033729 +mat:YVO4.n[6] 1.970968501779045 +mat:YVO4.n[7] 1.9660832466256781 +mat:YVO4.n[8] 1.962194263635589 +mat:YVO4.n[9] 1.9590128272858973 +mat:YVO4.sigma 0 +mat:aBBO-e.fR 0 +mat:aBBO-e.indexReference air +mat:aBBO-e.isGas False +mat:aBBO-e.n2 0 +mat:aBBO-e.n[0] 1.5475538353072276 +mat:aBBO-e.n[10] 1.5225946749293269 +mat:aBBO-e.n[11] 1.5220027892088421 +mat:aBBO-e.n[12] 1.521461749976414 +mat:aBBO-e.n[13] 1.5209580857766944 +mat:aBBO-e.n[14] 1.5204817074274792 +mat:aBBO-e.n[15] 1.520024935262143 +mat:aBBO-e.n[16] 1.519581838348557 +mat:aBBO-e.n[17] 1.5191477761759362 +mat:aBBO-e.n[18] 1.5187190747850576 +mat:aBBO-e.n[19] 1.5182927940245605 +mat:aBBO-e.n[1] 1.5395433678830392 +mat:aBBO-e.n[20] 1.5178665577277544 +mat:aBBO-e.n[21] 1.517438428070808 +mat:aBBO-e.n[22] 1.5170068114328379 +mat:aBBO-e.n[23] 1.516570387034293 +mat:aBBO-e.n[2] 1.534621124167395 +mat:aBBO-e.n[3] 1.5313434854894206 +mat:aBBO-e.n[4] 1.5290244558962016 +mat:aBBO-e.n[5] 1.5273016051031838 +mat:aBBO-e.n[6] 1.5259679163428022 +mat:aBBO-e.n[7] 1.5248978139177811 +mat:aBBO-e.n[8] 1.5240113818871535 +mat:aBBO-e.n[9] 1.5232557268659885 +mat:aBBO-e.sigma 0 +mat:aBBO.fR 0 +mat:aBBO.indexReference air +mat:aBBO.isGas False +mat:aBBO.n2 0 +mat:aBBO.n[0] 1.6768020667616064 +mat:aBBO.n[10] 1.6395922617036918 +mat:aBBO.n[11] 1.6387168814268194 +mat:aBBO.n[12] 1.6379276322369092 +mat:aBBO.n[13] 1.6372032351906116 +mat:aBBO.n[14] 1.6365276814720058 +mat:aBBO.n[15] 1.6358887388838736 +mat:aBBO.n[16] 1.635276929915229 +mat:aBBO.n[17] 1.6346848185262892 +mat:aBBO.n[18] 1.6341065034580708 +mat:aBBO.n[19] 1.6335372524243894 +mat:aBBO.n[1] 1.66530317273176 +mat:aBBO.n[20] 1.632973234122376 +mat:aBBO.n[21] 1.6324113192634744 +mat:aBBO.n[22] 1.6318489310275477 +mat:aBBO.n[23] 1.6312839313888008 +mat:aBBO.n[2] 1.6579847940085524 +mat:aBBO.n[3] 1.6530102556714468 +mat:aBBO.n[4] 1.6494495380959506 +mat:aBBO.n[5] 1.6467900549175942 +mat:aBBO.n[6] 1.6447301930225287 +mat:aBBO.n[7] 1.6430829785957666 +mat:aBBO.n[8] 1.6417274573803824 +mat:aBBO.n[9] 1.6405825536114258 +mat:aBBO.sigma 0 +raman:hollenbeck.h[0] 0 +raman:hollenbeck.h[10] -1285671799771.126 +raman:hollenbeck.h[11] -10281922589.365192 +raman:hollenbeck.h[12] 401333618191.7717 +raman:hollenbeck.h[13] -1013060945268.4017 +raman:hollenbeck.h[14] 1098122686045.1313 +raman:hollenbeck.h[15] -843077983240.1337 +raman:hollenbeck.h[16] 283164885048.4083 +raman:hollenbeck.h[17] 39151594757.25173 +raman:hollenbeck.h[18] -213975319424.21838 +raman:hollenbeck.h[19] -97873317197.07703 +raman:hollenbeck.h[1] 35989877128573.16 +raman:hollenbeck.h[20] 431862508443.6543 +raman:hollenbeck.h[21] -708243623966.745 +raman:hollenbeck.h[22] 618869080889.6959 +raman:hollenbeck.h[23] -283989336597.36395 +raman:hollenbeck.h[24] -149018384708.40903 +raman:hollenbeck.h[25] 438430667613.1034 +raman:hollenbeck.h[26] -444780954927.0145 +raman:hollenbeck.h[27] 235784589147.42178 +raman:hollenbeck.h[28] 56327043689.06896 +raman:hollenbeck.h[29] -221265485770.89816 +raman:hollenbeck.h[2] -10043883156182.17 +raman:hollenbeck.h[30] 202349759318.86627 +raman:hollenbeck.h[31] -19333910320.816223 +raman:hollenbeck.h[32] -179682890329.93134 +raman:hollenbeck.h[33] 279730173623.8054 +raman:hollenbeck.h[34] -222307985339.7368 +raman:hollenbeck.h[35] 58567851310.15957 +raman:hollenbeck.h[36] 109924737813.16307 +raman:hollenbeck.h[37] -190100037911.29095 +raman:hollenbeck.h[38] 151614845673.01096 +raman:hollenbeck.h[39] -36789021640.017494 +raman:hollenbeck.h[3] -3473922319549.194 +raman:hollenbeck.h[40] -77278899524.6551 +raman:hollenbeck.h[41] 124864838372.95995 +raman:hollenbeck.h[42] -90410858697.0895 +raman:hollenbeck.h[43] 6852699828.849853 +raman:hollenbeck.h[44] 69324255336.89061 +raman:hollenbeck.h[45] -95685243950.18474 +raman:hollenbeck.h[46] 65001665131.138885 +raman:hollenbeck.h[47] -3968636501.734851 +raman:hollenbeck.h[48] -48085834739.88096 +raman:hollenbeck.h[49] 63703225463.266365 +raman:hollenbeck.h[4] 7256794848807.441 +raman:hollenbeck.h[50] -40852656857.17132 +raman:hollenbeck.h[51] -445917412.69459945 +raman:hollenbeck.h[52] 33525236947.15049 +raman:hollenbeck.h[53] -41365692915.00596 +raman:hollenbeck.h[54] 24436136171.687183 +raman:hollenbeck.h[55] 2941134423.057782 +raman:hollenbeck.h[56] -23354899697.59598 +raman:hollenbeck.h[57] 26754977308.521446 +raman:hollenbeck.h[58] -14654477089.198107 +raman:hollenbeck.h[59] -3045532879.454758 +raman:hollenbeck.h[5] -2352072689636.209 +raman:hollenbeck.h[60] 15339479502.743563 +raman:hollenbeck.h[61] -16532227224.00643 +raman:hollenbeck.h[62] 8327879939.114692 +raman:hollenbeck.h[63] 2691451934.6223745 +raman:hollenbeck.h[64] -9798674789.98217 +raman:hollenbeck.h[65] 9918025813.781492 +raman:hollenbeck.h[66] -4543215126.543189 +raman:hollenbeck.h[67] -2112244299.2413967 +raman:hollenbeck.h[68] 6077759446.002182 +raman:hollenbeck.h[69] -5784423699.0510235 +raman:hollenbeck.h[6] -257286974620.81555 +raman:hollenbeck.h[70] 2387168452.5564575 +raman:hollenbeck.h[71] 1510601880.651677 +raman:hollenbeck.h[72] -3644454653.977025 +raman:hollenbeck.h[73] 3266161955.34037 +raman:hollenbeck.h[74] -1195240418.977689 +raman:hollenbeck.h[75] -1015263183.4321281 +raman:hollenbeck.h[76] 2118472264.521392 +raman:hollenbeck.h[77] -1787846515.7413638 +raman:hollenbeck.h[78] 567761712.5062599 +raman:hollenbeck.h[79] 646885444.2469707 +raman:hollenbeck.h[7] 2214160392573.809 +raman:hollenbeck.h[8] -2703928536469.638 +raman:hollenbeck.h[9] 2285595711590.075 +raman:linAgrawal.h[0] 4374999999999.9995 +raman:linAgrawal.h[10] -67818467229.76584 +raman:linAgrawal.h[11] -114995553387.45093 +raman:linAgrawal.h[12] -111374190691.45259 +raman:linAgrawal.h[13] -99752641563.34256 +raman:linAgrawal.h[14] -94496206652.93341 +raman:linAgrawal.h[15] -84260053377.65082 +raman:linAgrawal.h[16] -73210914907.10524 +raman:linAgrawal.h[17] -63476504581.71685 +raman:linAgrawal.h[18] -54184970329.13902 +raman:linAgrawal.h[19] -45748057637.59564 +raman:linAgrawal.h[1] 33076780115149.387 +raman:linAgrawal.h[20] -38396945777.98691 +raman:linAgrawal.h[21] -31998857822.39913 +raman:linAgrawal.h[22] -26508016183.696 +raman:linAgrawal.h[23] -21859125328.97464 +raman:linAgrawal.h[24] -17947662666.562046 +raman:linAgrawal.h[25] -14679820828.570076 +raman:linAgrawal.h[26] -11967577055.90959 +raman:linAgrawal.h[27] -9727316146.065987 +raman:linAgrawal.h[28] -7885132442.23413 +raman:linAgrawal.h[29] -6376402573.117417 +raman:linAgrawal.h[2] -10784261997909.117 +raman:linAgrawal.h[30] -5145004903.648631 +raman:linAgrawal.h[31] -4143068097.2118645 +raman:linAgrawal.h[32] -3330118387.0963078 +raman:linAgrawal.h[33] -2672159707.980955 +raman:linAgrawal.h[34] -2140853621.609842 +raman:linAgrawal.h[35] -1712711733.5840106 +raman:linAgrawal.h[36] -1368356730.9945965 +raman:linAgrawal.h[37] -1091873965.2468326 +raman:linAgrawal.h[38] -870242363.2296818 +raman:linAgrawal.h[39] -692843682.8613437 +raman:linAgrawal.h[3] 258098303201.55286 +raman:linAgrawal.h[40] -551045342.2395997 +raman:linAgrawal.h[41] -437848085.3344353 +raman:linAgrawal.h[42] -347590758.4739604 +raman:linAgrawal.h[43] -275704665.3108277 +raman:linAgrawal.h[44] -218510212.0884979 +raman:linAgrawal.h[45] -173049366.07034495 +raman:linAgrawal.h[46] -136948208.88912115 +raman:linAgrawal.h[47] -108304585.35496739 +raman:linAgrawal.h[48] -85596565.04935096 +raman:linAgrawal.h[49] -67608080.38325554 +raman:linAgrawal.h[4] 3808837564674.6973 +raman:linAgrawal.h[50] -53368676.14533688 +raman:linAgrawal.h[51] -42104808.60254044 +raman:linAgrawal.h[52] -33200565.416268535 +raman:linAgrawal.h[53] -26166046.453827824 +raman:linAgrawal.h[54] -20611957.40357619 +raman:linAgrawal.h[55] -16229229.4427516 +raman:linAgrawal.h[56] -12772695.787118254 +raman:linAgrawal.h[57] -10048036.164673455 +raman:linAgrawal.h[58] -7901348.778374935 +raman:linAgrawal.h[59] -6210831.208748316 +raman:linAgrawal.h[5] -676741973836.3364 +raman:linAgrawal.h[60] -4880151.369690713 +raman:linAgrawal.h[61] -3833170.8515789397 +raman:linAgrawal.h[62] -3009748.9817967964 +raman:linAgrawal.h[63] -2362409.416458364 +raman:linAgrawal.h[64] -1853694.316646982 +raman:linAgrawal.h[65] -1454066.0438222827 +raman:linAgrawal.h[66] -1140244.3919279191 +raman:linAgrawal.h[67] -893889.9419882407 +raman:linAgrawal.h[68] -700562.2311111744 +raman:linAgrawal.h[69] -548895.9315925267 +raman:linAgrawal.h[6] 17566331646.613495 +raman:linAgrawal.h[70] -429949.83732214005 +raman:linAgrawal.h[71] -336692.72234480456 +raman:linAgrawal.h[72] -263597.530564266 +raman:linAgrawal.h[73] -206321.24812813866 +raman:linAgrawal.h[74] -161452.50086143587 +raman:linAgrawal.h[75] -126312.64952316385 +raman:linAgrawal.h[76] -98799.1194643596 +raman:linAgrawal.h[77] -77262.05389727734 +raman:linAgrawal.h[78] -60407.24589975991 +raman:linAgrawal.h[79] -47219.782990001186 +raman:linAgrawal.h[7] 368620715680.38696 +raman:linAgrawal.h[8] -113333707930.66406 +raman:linAgrawal.h[9] -98107105669.04967 +raman:orientational.h[0] 0 +raman:orientational.h[10] 406314664605.16595 +raman:orientational.h[11] 420889165241.88275 +raman:orientational.h[12] 432784970022.8361 +raman:orientational.h[13] 442331812727.2469 +raman:orientational.h[14] 449820450170.4397 +raman:orientational.h[15] 455507245718.1196 +raman:orientational.h[16] 459618214167.7087 +raman:orientational.h[17] 462352591288.64307 +raman:orientational.h[18] 463885983876.5307 +raman:orientational.h[19] 464373149612.92487 +raman:orientational.h[1] 72856810300.5807 +raman:orientational.h[20] 463950450230.53064 +raman:orientational.h[21] 462738016372.2774 +raman:orientational.h[22] 460841658021.922 +raman:orientational.h[23] 458354550403.106 +raman:orientational.h[24] 455358721730.7915 +raman:orientational.h[25] 451926366098.801 +raman:orientational.h[26] 448121002051.2612 +raman:orientational.h[27] 443998494971.3111 +raman:orientational.h[28] 439607959289.6954 +raman:orientational.h[29] 434992554635.4975 +raman:orientational.h[2] 136023179574.9566 +raman:orientational.h[30] 430190188391.8443 +raman:orientational.h[31] 425234135654.9846 +raman:orientational.h[32] 420153586302.7851 +raman:orientational.h[33] 414974127738.18677 +raman:orientational.h[34] 409718170866.6766 +raman:orientational.h[35] 404405325978.6073 +raman:orientational.h[36] 399052734423.3434 +raman:orientational.h[37] 393675361270.4635 +raman:orientational.h[38] 388286253542.7855 +raman:orientational.h[39] 382896768067.24316 +raman:orientational.h[3] 190655276555.8687 +raman:orientational.h[40] 377516772514.2177 +raman:orientational.h[41] 372154822776.3563 +raman:orientational.h[42] 366818319467.6485 +raman:orientational.h[43] 361513645996.7696 +raman:orientational.h[44] 356246290380.3406 +raman:orientational.h[45] 351020952707.27344 +raman:orientational.h[46] 345841639940.7924 +raman:orientational.h[47] 340711749546.5372 +raman:orientational.h[48] 335634143260.2487 +raman:orientational.h[49] 330611212154.19525 +raman:orientational.h[4] 237773145072.03024 +raman:orientational.h[50] 325644934025.27606 +raman:orientational.h[51] 320736924007.5427 +raman:orientational.h[52] 315888479205.78735 +raman:orientational.h[53] 311100618053.2359 +raman:orientational.h[54] 306374115013.7651 +raman:orientational.h[55] 301709531176.1537 +raman:orientational.h[56] 297107241223.53845 +raman:orientational.h[57] 292567457204.46094 +raman:orientational.h[58] 288090249481.7854 +raman:orientational.h[59] 283675565191.54517 +raman:orientational.h[5] 278276703355.1532 +raman:orientational.h[60] 279323244504.7508 +raman:orientational.h[61] 275033034950.7538 +raman:orientational.h[62] 270804604030.36707 +raman:orientational.h[63] 266637550320.1239 +raman:orientational.h[64] 262531413245.38556 +raman:orientational.h[65] 258485681679.1206 +raman:orientational.h[66] 254499801504.7466 +raman:orientational.h[67] 250573182265.15546 +raman:orientational.h[68] 246705203005.68848 +raman:orientational.h[69] 242895217406.15945 +raman:orientational.h[6] 312959863313.5853 +raman:orientational.h[70] 239142558285.84225 +raman:orientational.h[71] 235446541555.47385 +raman:orientational.h[72] 231806469681.61642 +raman:orientational.h[73] 228221634721.03915 +raman:orientational.h[74] 224691320975.99826 +raman:orientational.h[75] 221214807315.31082 +raman:orientational.h[76] 217791369200.83682 +raman:orientational.h[77] 214420280454.32407 +raman:orientational.h[78] 211100814795.45706 +raman:orientational.h[79] 207832247178.323 +raman:orientational.h[7] 342522990683.31573 +raman:orientational.h[8] 367583901009.3951 +raman:orientational.h[9] 388687563503.20953 +raman:silica.h[0] 0 +raman:silica.h[10] 37891675258.60172 +raman:silica.h[11] -9088365606.445019 +raman:silica.h[12] -4111573547.5928917 +raman:silica.h[13] 3638146801.4285297 +raman:silica.h[14] -671729090.5821713 +raman:silica.h[15] -479447889.30336934 +raman:silica.h[16] 342900269.87257797 +raman:silica.h[17] -44042535.18790392 +raman:silica.h[18] -53310869.69707302 +raman:silica.h[19] 31703562.475412227 +raman:silica.h[1] 38156822899006.04 +raman:silica.h[20] -2189208.194580782 +raman:silica.h[21] -5722626.601085337 +raman:silica.h[22] 2871100.3953604843 +raman:silica.h[23] -10706.497803657006 +raman:silica.h[24] -597302.3038872388 +raman:silica.h[25] 254020.72680886672 +raman:silica.h[26] 18125.84351552139 +raman:silica.h[27] -60886.09271795594 +raman:silica.h[28] 21865.491385953057 +raman:silica.h[29] 3545.6158091514135 +raman:silica.h[2] -16083969765245.887 +raman:silica.h[30] -6077.813785041424 +raman:silica.h[31] 1818.735467288001 +raman:silica.h[32] 507.34056232980015 +raman:silica.h[33] -595.0832644221234 +raman:silica.h[34] 144.49678825787223 +raman:silica.h[35] 63.82754177110512 +raman:silica.h[36] -57.19293457318968 +raman:silica.h[37] 10.729144760228479 +raman:silica.h[38] 7.465711508879934 +raman:silica.h[39] -5.395918327513887 +raman:silica.h[3] -1218345054339.048 +raman:silica.h[40] 0.7096043063218415 +raman:silica.h[41] 0.8319315512460381 +raman:silica.h[42] -0.4994192367059566 +raman:silica.h[43] 0.036134254655386416 +raman:silica.h[44] 0.08945254778356916 +raman:silica.h[45] -0.045280437160581555 +raman:silica.h[46] 0.0003364627714771777 +raman:silica.h[47] 0.009349468523875578 +raman:silica.h[48] -0.00401154031867023 +raman:silica.h[49] -0.0002687994706535023 +raman:silica.h[4] 3884943398143.1646 +raman:silica.h[50] 0.0009541696288343015 +raman:silica.h[51] -0.00034586079714891987 +raman:silica.h[52] -5.4216617481374955E-05 +raman:silica.h[53] 9.534990034951208E-05 +raman:silica.h[54] -2.882773392369138E-05 +raman:silica.h[55] -7.834878558345925E-06 +raman:silica.h[56] 9.345201025626478E-06 +raman:silica.h[57] -2.296935279948748E-06 +raman:silica.h[58] -9.906500235089946E-07 +raman:silica.h[59] 8.990453343772384E-07 +raman:silica.h[5] -1382213114005.2488 +raman:silica.h[60] -1.7131655603613194E-07 +raman:silica.h[61] -1.1623631003310734E-07 +raman:silica.h[62] 8.490615258360499E-08 +raman:silica.h[63] -1.1425422627202045E-08 +raman:silica.h[64] -1.298122060178259E-08 +raman:silica.h[65] 7.866778779772512E-09 +raman:silica.h[66] -5.950147419731122E-10 +raman:silica.h[67] -1.398154148925782E-09 +raman:silica.h[68] 7.140756782039504E-10 +raman:silica.h[69] -7.930128801888032E-12 +raman:silica.h[6] -231694071155.4879 +raman:silica.h[70] -1.4633566356302637E-10 +raman:silica.h[71] 6.334605986955864E-11 +raman:silica.h[72] 3.971814667428607E-12 +raman:silica.h[73] -1.4952265808938753E-11 +raman:silica.h[74] 5.47018295192547E-12 +raman:silica.h[75] 8.283584951400076E-13 +raman:silica.h[76] -1.4957846057379467E-12 +raman:silica.h[77] 4.568739003417348E-13 +raman:silica.h[78] 1.2095073308792888E-13 +raman:silica.h[79] -1.4674946030989284E-13 +raman:silica.h[7] 387391935331.8114 +raman:silica.h[8] -114728806844.29845 +raman:silica.h[9] -32840956228.315388 +ramanfraction:blowWood 0.18 +ramanfraction:hollenbeck 0.18 +ramanfraction:linAgrawal 0.245 +ramanfraction:other NaN +ramanfraction:silica 0.18 diff --git a/tests/UltrafastEngine.Tests/IonizationTests.cs b/tests/UltrafastEngine.Tests/IonizationTests.cs index 778b4bf..9457063 100644 --- a/tests/UltrafastEngine.Tests/IonizationTests.cs +++ b/tests/UltrafastEngine.Tests/IonizationTests.cs @@ -70,6 +70,177 @@ public void Plasma_ElectronDensityBlueshiftAndAbsorption() Assert.Greater(SpectralCentroid(r.Output), SpectralCentroid(r.Input), "plasma blue-shifts the pulse"); Assert.True(rec.LossJ.ContainsKey("absorption") && rec.LossJ["absorption"] > 0, "ionisation takes energy from the pulse"); } + [Test(Reference = "Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007), eq. 6 and 15: the avalanche is fed by inverse-bremsstrahlung absorption, which the Drude imaginary part already is", Tolerance = "1e-4")] + public void Plasma_Avalanche_DoesNotAbsorbTheFieldASecondTime() + { + // A weak pulse (about 5e11 W/m2) in a prepared plasma that absorbs 33 % of the amplitude: the avalanche cannot change the density + // over the pulse, so the output energy must be the same with it on or off. Counting the energy twice would double the absorption. + var off = PreparedPlasma(1e24, 0.01, 10e-15, false, avalanche: false); + var on = PreparedPlasma(1e24, 0.01, 10e-15, false, avalanche: true); + double eOff = off.Output.TotalEnergyJ(), eOn = on.Output.TotalEnergyJ(); + Assert.Less(eOff / off.Input.TotalEnergyJ(), 0.9, "the plasma absorbs (the test can tell a double count)"); + Assert.Close(eOff, eOn, 1e-4, 0, "output energy with and without the avalanche"); + } + + // ----------------------------------------------------------------------------------------------- Drude dispersion + + private const double E0 = 1.602176634e-19, Me = 9.1093837015e-31, Eps0 = 8.8541878128e-12, Cc = 2.99792458e8; + + /// A prepared plasma of fixed density in a dispersionless n = 1.45 solid: no ionisation (sigmaK = 0), no Kerr. + private static RunResult PreparedPlasma(double rho, double lenM, double fwhmS, bool carrierResolved, bool freqResolved = true, double tauC = 20e-15, double lam = 800e-9, bool avalanche = false) + { + var mat = FlatMaterial(0.0, 1.45); + var ion = O("model", "mpi", "sigmaK", 0.0, "photons", 6, "bandGapEv", 9.0, "neutralDensityM3", 2e28, "electronDensityM3", rho, "collisionTimeS", tauC, "recombinationTimeS", 1e30, "avalanche", avalanche, "frequencyResolved", freqResolved); + var sys = Sys(O("id", "cell", "type", "bulk", "material", "m", "lengthM", lenM, "beam", O("kind", "constant", "areaM2", 1e-7), "ionization", ion)); + var o = O("format", "ultrafast-system", "version", 2, "name", "t", "materials", O("m", mat)); + o.Set("elements", sys.ToJson()["elements"]); + var extra = carrierResolved ? O("physics", O("selfSteepening", false, "fieldMode", "carrier")) : O("physics", O("selfSteepening", false)); + extra.Set("stepper", O("tolerance", 1e-11)); + return Go(SystemDescription.FromJson(o), Spec(Gauss(lam, fwhmS, 1e-9), extra)); + } + + private static RunResult IonisingGas(bool carrier, bool freqResolved, double lenM) + { + var o = O("format", "ultrafast-system", "version", 2, "name", "cell", "materials", O("argon", O("gas", O("species", "Ar", "pressureBar", 1.0), "n2", 0.0))); + o.Set("elements", A(O("id", "cell", "type", "bulk", "material", "argon", "lengthM", lenM, "beam", O("kind", "constant", "areaM2", 1e-7), + "ionization", O("model", "mpi", "photons", 4, "sigmaK", 1e11 / Math.Pow(1e18, 4), "avalanche", false, "frequencyResolved", freqResolved, "recombinationTimeS", 1e30)))); + var run = O("format", "ultrafast-run", "version", 2, "physics", carrier ? O("selfSteepening", false, "fieldMode", "carrier") : O("selfSteepening", false)); + run.Set("pulse", O("shape", "gaussian", "centerWavelengthM", 2e-6, "durationFwhmS", 10e-15, "energyJ", 1.1e-3)); + return Simulation.Run(SystemDescription.FromJson(o), Specs.RunSpec.FromJson(run)); + } + + [Test(Reference = "drift energy of electrons born at rest in a linearly polarised field: U_p = e² E0² / (4 m ω²) per electron (Corkum, Phys. Rev. Lett. 71, 1994 (1993))", Tolerance = "2 %")] + public void Plasma_FrequencyResolved_TakesTheDriftEnergyOfTheElectronsBornInTheField() + { + // The electron current obeys dJ/dt = (e²/m) ρ(t) E, so electrons born in the pulse carry kinetic energy; after a long pulse the + // part that stays is the drift energy, U_p of the field at birth (cycle-averaged over the birth phase). The carrier-frequency form + // Δn(t) E(t) is a pure phase modulation and misses it. A 150 fs, 800 nm pulse (75 cycles) in argon with a 4-photon rate, in a 20 µm cell + // (weak plasma: the first-order result holds to 0.1 %): the extra energy loss of the frequency-resolved run over the carrier-value run is + // ∫ ρ̇(t) U_p(t) dt x area x length. The exact first-order energy change of the input pulse under the operator agrees with U_p to 0.3 %. + double L = 2e-5, area = 1e-7, lam = 800e-9, w0 = 2 * Math.PI * Cc / lam, sig = 1e11 / Math.Pow(1e18, 4), N = 1e5 / (1.380649e-23 * 293.15); + RunResult Run(bool fr) + { + var o = O("format", "ultrafast-system", "version", 2, "name", "cell", "materials", O("argon", O("gas", O("species", "Ar", "pressureBar", 1.0), "n2", 0.0))); + o.Set("elements", A(O("id", "cell", "type", "bulk", "material", "argon", "lengthM", L, "beam", O("kind", "constant", "areaM2", area), + "ionization", O("model", "mpi", "photons", 4, "sigmaK", sig, "avalanche", false, "frequencyResolved", fr, "recombinationTimeS", 1e30, "collisionTimeS", 1e-3)))); + var run = O("format", "ultrafast-run", "version", 2, "physics", O("selfSteepening", false), "stepper", O("tolerance", 1e-10)); + run.Set("pulse", O("shape", "gaussian", "centerWavelengthM", lam, "durationFwhmS", 150e-15, "energyJ", 1.596e-2)); + return Simulation.Run(SystemDescription.FromJson(o), Specs.RunSpec.FromJson(run)); + } + var a = Run(true); var b = Run(false); + double extra = b.Output.TotalEnergyJ() - a.Output.TotalEnergyJ(), heat = 0, dt = a.Input.Grid.DTime; + foreach (var p in PulseMetrics.TemporalPower(a.Input)) + { + double I = p / area, rate = sig * I * I * I * I * N, up = E0 * E0 * I / (2 * Me * Eps0 * Cc * w0 * w0); + heat += rate * up * dt; + } + heat *= area * L; + Assert.Greater(heat, 1e-7, "a measurable heating energy"); + Assert.Close(heat, extra, 0.02, 0, "extra absorbed energy = integral of (birth rate x U_p)"); + // the carrier-value run takes only the ionisation energy + double ionisation = 0; foreach (var p in PulseMetrics.TemporalPower(a.Input)) ionisation += sig * Math.Pow(p / area, 4) * N * 15.76 * E0 * dt; + Assert.Close(ionisation * area * L, b.Input.TotalEnergyJ() - b.Output.TotalEnergyJ(), 0.03, 0, "ionisation energy U_i per electron, both runs"); + } + + private static System.Numerics.Complex DrudeFactor(double rho, double n0, double w, double nu, double lenM) => + System.Numerics.Complex.Exp(System.Numerics.Complex.ImaginaryOne * (w / Cc) * (-rho * E0 * E0 / (2 * n0 * Eps0 * Me) / (w * new System.Numerics.Complex(w, nu))) * lenM); + + [Test(Reference = "Drude response of free electrons: Δn(ω) = -ω_p² / (2 n0 ω (ω + iν)) (Couairon & Mysyrowicz, Phys. Rep. 441, 47 (2007))", Tolerance = "1e-8 (solver tolerance 1e-11), both field models")] + public void Plasma_FixedDensity_FollowsTheFrequencyResolvedDrudeIndex_InEnvelopeAndCarrierMode() + { + // A prepared plasma of 1e24 /m^3 (no ionisation, no recombination) acts as a linear filter exp(i (ω/c) Δn(ω) L) on a 10 fs, 800 nm + // pulse: 15.5 rad of phase and 33 % amplitude absorption at the carrier, with the 1/ω² dependence across the 0.13 ω0 bandwidth. The ratio of the + // output spectra with and without the plasma must be that filter. Before the frequency-resolved operator the envelope used the carrier value + // and carrier-resolved mode weighted it by ω / ω0²; frequencyResolved: false (below) is the carrier value and differs by far more than the tolerance. + double rho = 1e24, L = 0.01, nu = 1 / 20e-15, n0 = 1.45; + foreach (bool carrier in new[] { false, true }) + { + var with = PreparedPlasma(rho, L, 10e-15, carrier); var without = PreparedPlasma(0, L, 10e-15, carrier); + var w = with.Output.Grid.OmegaAbsolute; double amax = without.Output.Ax.Max(z => z.Magnitude), worst = 0, worstOld = 0; int used = 0; + var old = PreparedPlasma(rho, L, 10e-15, carrier, false); + for (int j = 0; j < w.Length; j++) + { + if (without.Output.Ax[j].Magnitude < 0.01 * amax || w[j] <= 0) continue; + var f = DrudeFactor(rho, n0, w[j], nu, L); + worst = Math.Max(worst, (with.Output.Ax[j] / without.Output.Ax[j] - f).Magnitude); + worstOld = Math.Max(worstOld, (old.Output.Ax[j] / without.Output.Ax[j] - f).Magnitude); + used++; + } + Assert.Greater(used, 30, "bins above 1 % of the peak"); + Assert.Less(worst, 1e-8, (carrier ? "carrier-resolved" : "envelope") + ": output / input spectrum against the Drude filter"); + Assert.Greater(worstOld, 0.1, "frequencyResolved: false is the carrier-frequency value, which this test can tell apart"); + } + } + + [Test(Reference = "the same Drude filter in the radial spatial model (a uniform plasma commutes with diffraction): energy transmission", Tolerance = "1e-7")] + public void Plasma_FixedDensity_SpatialModel_AppliesTheSameDrudeFilter() + { + // The plasma is uniform, so it commutes with the diffraction of a wide beam: the energy with and without it differ by the + // spectrum-weighted |filter|² of the input pulse, exactly as in the 1-D model. + double rho = 1e24, L = 0.01, nu = 1 / 20e-15, n0 = 1.45; + RunResult Run(double d, bool fr) + { + var ion = O("model", "mpi", "sigmaK", 0.0, "photons", 6, "bandGapEv", 9.0, "neutralDensityM3", 2e28, "electronDensityM3", d, "collisionTimeS", 20e-15, "recombinationTimeS", 1e30, "avalanche", false, "frequencyResolved", fr); + var o = O("format", "ultrafast-system", "version", 2, "name", "t", "materials", O("m", FlatMaterial(0.0, 1.45))); + o.Set("elements", A(O("id", "cell", "type", "bulk", "material", "m", "lengthM", L, "beam", O("kind", "gaussian", "w0M", 1.5e-3), + "transverse", O("model", "radial", "points", 32, "radiusM", 5e-3), "ionization", ion))); + var run = O("format", "ultrafast-run", "version", 2, "physics", O("selfSteepening", false), "stepper", O("tolerance", 1e-10)); + run.Set("pulse", O("shape", "gaussian", "centerWavelengthM", 800e-9, "durationFwhmS", 10e-15, "energyJ", 1e-9)); + return Simulation.Run(SystemDescription.FromJson(o), Specs.RunSpec.FromJson(run)); + } + var with = Run(rho, true); var without = Run(0, true); var old = Run(rho, false); + var g = with.Input.Grid; double num = 0, den = 0; + for (int j = 0; j < g.N; j++) + { + if (!(g.OmegaAbsolute[j] > 0)) continue; + double pw = with.Input.Ax[j].Magnitude * with.Input.Ax[j].Magnitude, f = DrudeFactor(rho, n0, g.OmegaAbsolute[j], nu, L).Magnitude; + num += pw * f * f; den += pw; + } + double expected = num / den, got = with.Output.TotalEnergyJ() / without.Output.TotalEnergyJ(), gotOld = old.Output.TotalEnergyJ() / without.Output.TotalEnergyJ(); + Assert.Close(expected, got, 1e-7, 0, "energy transmission of the plasma, spatial model"); + Assert.Greater(Math.Abs(gotOld / expected - 1), 1e-3, "the carrier-frequency value would differ (this test can tell them apart)"); + } + + [Test(Reference = "narrow-band limit: the frequency-resolved operator tends to the carrier value as the bandwidth goes to zero; R(ω0) = 1", Tolerance = "1e-15; difference halves with the bandwidth")] + public void Plasma_FrequencyResolved_TendsToTheCarrierValue_ForNarrowSpectra() + { + var pl = new PlasmaResponse { CollisionTimeS = 20e-15 }; + double w0 = W(800e-9); + Assert.Close(1, pl.Spectral(w0, w0).Real, 0, 1e-15, "R(ω0), real part"); Assert.Close(0, pl.Spectral(w0, w0).Imaginary, 0, 1e-15, "R(ω0), imaginary part"); + Assert.Close(0, pl.Spectral(-w0, w0).Magnitude, 0, 0, "no negative frequencies"); + // the output of the two operators differs at first order in the bandwidth: 40 fs against 80 fs + double Diff(double fwhm) + { + var a = PreparedPlasma(1e23, 0.003, fwhm, false); var b = PreparedPlasma(1e23, 0.003, fwhm, false, false); + double d = 0, n = 0; for (int j = 0; j < a.Output.Ax.Length; j++) { var x = a.Output.Ax[j] - b.Output.Ax[j]; d += x.Magnitude * x.Magnitude; n += a.Output.Ax[j].Magnitude * a.Output.Ax[j].Magnitude; } + return Math.Sqrt(d / n); + } + double d40 = Diff(40e-15), d80 = Diff(80e-15); + Assert.Close(2, d40 / d80, 0.1, 0, "difference between the two operators scales with the bandwidth"); + Assert.Less(d80, 0.05, "and is small for an 80 fs pulse"); + } + + [Test(Reference = "cross-check: the envelope model against the carrier-resolved field model, same operator", Tolerance = "blueshift 2 %, energy 1 %, density 1e-4")] + public void Plasma_EnvelopeAgreesWithCarrierResolved_ForA10FsMidInfraredPulse() + { + // 10 fs, 2 µm pulse in argon (MPI rate of order 4, so both models share the rate: PPT and ADK change to the quasi-static rate in carrier mode). + // The blueshift (12 % of the carrier at 2 cm without the 1/ω² dependence, 7 % with it) and the ionisation loss agree between the models. + foreach (double L in new[] { 0.02, 0.1 }) + { + var e = IonisingGas(false, true, L); var c = IonisingGas(true, true, L); + double w0 = e.Input.Grid.ReportOmega; + double se = (SpectralCentroid(e.Output) - SpectralCentroid(e.Input)) / w0, sc = (SpectralCentroid(c.Output) - SpectralCentroid(c.Input)) / w0; + double le = 1 - e.Output.TotalEnergyJ() / e.Input.TotalEnergyJ(), lc = 1 - c.Output.TotalEnergyJ() / c.Input.TotalEnergyJ(); + Assert.Greater(se, 0.05, "blueshift (fraction of the carrier) at L = " + L); + Assert.Close(sc, se, 0.02, 0, "blueshift, envelope vs carrier-resolved, L = " + L); + Assert.Close(lc, le, 0.01, 0, "energy loss, envelope vs carrier-resolved, L = " + L); + Assert.Close(e.Records[0].Values["peakElectronDensityM3"], c.Records[0].Values["peakElectronDensityM3"], 1e-4, 0, "peak electron density"); + var o = IonisingGas(false, false, L); + double so = (SpectralCentroid(o.Output) - SpectralCentroid(o.Input)) / w0; + Assert.Greater(Math.Abs(so / se - 1), 0.2, "the carrier-frequency Drude term overestimates the blueshift (L = " + L + ")"); + } + } + private static RunResult SpatialCell(string equation, bool plasma, double energy) { var o = O("format", "ultrafast-system", "version", 2, "name", "fil", "materials", O("argon", O("gas", O("species", "Ar", "pressureBar", 1.0)))); diff --git a/tests/UltrafastEngine.Tests/ModulatorTests.cs b/tests/UltrafastEngine.Tests/ModulatorTests.cs index bd4e2c2..6b986be 100644 --- a/tests/UltrafastEngine.Tests/ModulatorTests.cs +++ b/tests/UltrafastEngine.Tests/ModulatorTests.cs @@ -138,7 +138,7 @@ public void Maps_InlineFileAndCallback_AreIdentical() finally { System.IO.File.Delete(file); } } - [Test(Reference = "a continuous-wave source and a pulse at the carrier see the same (monochromatic) device: the cw focal fluence pattern equals that of a 1 ns pulse", Tolerance = "2e-5 of the peak: a 1 ns pulse has a 0.4 GHz bandwidth, so the optical-path phase differs from the carrier's by ~1e-6 of its value across the band")] + [Test(Reference = "a continuous-wave source and a pulse at the carrier see the same (monochromatic) device: the cw focal fluence pattern equals that of a 1 ns pulse", Tolerance = "1e-7 of the peak (measured 1.3e-8): a 1 ns pulse has a 0.4 GHz bandwidth, so the optical-path phase differs from the carrier's by ~1e-6 of its value across the band; the cw line sits on the requested carrier")] public void CwRun_EqualsThePulse_AtTheCarrier() { double k = 2 * Math.PI / Lam; var mod = Mod(Map(i => k * 3e-3 * X(i))); @@ -147,7 +147,7 @@ public void CwRun_EqualsThePulse_AtTheCarrier() var cw = Go(Sys(Source(0.3e-3), Mod(Map(i => k * 3e-3 * X(i))), Lens(F), Free("z", F)), Spec(O("shape", "cw", "centerWavelengthM", Lam, "powerW", 1.0), O("instruments", A(Bp("z"))))); var a = Arr(pulse, "fluence_x_j_m2"); var b = Arr(cw, "fluence_x_j_m2"); double pa = a.Max(), pb = b.Max(); - for (int i = 0; i < a.Length; i++) Assert.Close(a[i] / pa, b[i] / pb, 0, 2e-5, "pattern"); + for (int i = 0; i < a.Length; i++) Assert.Close(a[i] / pa, b[i] / pb, 0, 1e-7, "pattern"); } [Test(Reference = "the interpolation error of the stored frequencies is 0 for a phase-free device and falls as the number of stored frequencies grows (compare K and 2K); too few frequencies warn", Tolerance = "error ratio 2.5-6 per doubling of K (1/K^2); below the warning level 0.05 at K = 32; warning at K = 3")] @@ -345,17 +345,18 @@ public void Trials_WithACallbackMap_EqualRunsFromMapFiles() } } - [Test(Reference = "one stored frequency (spectralSamples = 1) sits at the energy centroid of the spectrum (the carrier of a 1 ns Gaussian, within one grid bin); the cw source's single bin is snapped to its grid lattice, 1.2e-6 from the requested carrier (the run notes print it), so the monochromatic focal patterns agree to the pattern change of that offset", Tolerance = "stored frequency within 1e-7 of the carrier; pattern 3e-6 of the peak")] + [Test(Reference = "one stored frequency (spectralSamples = 1) sits at the energy centroid of the spectrum (the carrier of a 1 ns Gaussian, within one grid bin); the cw source puts its single line on the requested carrier (the lattice is shifted; it was snapped, 1.2e-6 away, until 2.2.1), so the two monochromatic focal patterns are the same", Tolerance = "stored frequencies within 1e-9 of the carrier; pattern 1e-9 of the peak (measured 1e-16)")] public void OneStoredFrequency_IsAtTheCarrier_AndTheCwPatternMatches() { double k = 2 * Math.PI / Lam; JsonObject One(JsonObject el) { el["transverse"].AsObject().Set("spectralSamples", 1); return el; } RunResult R(JsonObject pulse) => Go(Sys(One(Source(0.3e-3)), Mod(Map(i => k * 3e-3 * X(i))), Lens(F), One(Free("z", F))), Spec(pulse, O("instruments", A(Bp("z"))))); var rp = R(Gauss(Lam, 1e-9, 1e-3)); var rc = R(O("shape", "cw", "centerWavelengthM", Lam, "powerW", 1.0)); - Assert.Close(W(Lam), rp.At["src"].Profile.Omega[0], 1e-7, 0, "the stored frequency of the pulse is its carrier (it was the low edge of the band before 2026-11-10)"); + Assert.Close(W(Lam), rp.At["src"].Profile.Omega[0], 1e-9, 0, "the stored frequency of the pulse is its carrier (it was the low edge of the band before 2026-11-10)"); var a = Arr(rp, "fluence_x_j_m2"); var b = Arr(rc, "fluence_x_j_m2"); double pa = a.Max(), pb = b.Max(); - for (int i = 0; i < a.Length; i++) Assert.Close(a[i] / pa, b[i] / pb, 0, 3e-6, "pattern"); + Assert.Close(W(Lam), rc.At["src"].Profile.Omega[0], 1e-9, 0, "the cw line sits on the requested carrier"); + for (int i = 0; i < a.Length; i++) Assert.Close(a[i] / pa, b[i] / pb, 0, 1e-9, "pattern"); } [Test(Reference = "the reported spectralSamplingError (a whole-bin interpolation residual) bounds the true error of the focal fluence map with K stored frequencies, measured against K = 64 (spike S2)", Tolerance = "bound holds at K = 8, 16, 32; the true error falls with K")] @@ -381,7 +382,7 @@ public void SpectralSamplingError_BoundsTheTrueErrorOfTheFocalMap() } } - [Test(Reference = "naming (plan-ao-digital-twin-2026-11.md): \"phaseAtDesign\" is a phase given at the design wavelength and scaled with omega, the opd rule", Tolerance = "exact")] + [Test(Reference = "naming (history.md): \"phaseAtDesign\" is a phase given at the design wavelength and scaled with omega, the opd rule", Tolerance = "exact")] public void PhaseAtDesign_IsTheOpdRule() { var map = Map(i => 2 * Math.PI * (i % 16) / 16); diff --git a/tests/UltrafastEngine.Tests/SchemaTests.cs b/tests/UltrafastEngine.Tests/SchemaTests.cs index f48b818..6e58ed6 100644 --- a/tests/UltrafastEngine.Tests/SchemaTests.cs +++ b/tests/UltrafastEngine.Tests/SchemaTests.cs @@ -97,6 +97,7 @@ public void CommittedSchemasAndReference_AreUpToDate() Assert.Equal(Norm(SchemaGenerator.SystemSchema(Shipped).ToJson(true)), Norm(File.ReadAllText(Path.Combine(root, "schemas", "ufs.schema.json"))), "schemas/ufs.schema.json is current (ultrafast schema system -o schemas/ufs.schema.json)"); Assert.Equal(Norm(SchemaGenerator.RunSchema(Shipped).ToJson(true)), Norm(File.ReadAllText(Path.Combine(root, "schemas", "run.schema.json"))), "schemas/run.schema.json is current (ultrafast schema run -o schemas/run.schema.json)"); Assert.Equal(Norm(ReferenceGenerator.Markdown(Shipped)), Norm(File.ReadAllText(Path.Combine(root, "docs", "reference", "options.md"))), "docs/reference/options.md is current (ultrafast reference -o docs/reference/options.md)"); + Assert.Equal(Norm(UltrafastEngine.Materials.CatalogueReference.Markdown()), Norm(File.ReadAllText(Path.Combine(root, "docs", "reference", "materials-catalogue.md"))), "docs/reference/materials-catalogue.md is current (ultrafast reference materials -o docs/reference/materials-catalogue.md)"); } [Test(Category = "schema")] diff --git a/tests/UltrafastEngine.Tests/SpatialStreamingTests.cs b/tests/UltrafastEngine.Tests/SpatialStreamingTests.cs index 65b513c..4b95ea2 100644 --- a/tests/UltrafastEngine.Tests/SpatialStreamingTests.cs +++ b/tests/UltrafastEngine.Tests/SpatialStreamingTests.cs @@ -121,7 +121,7 @@ public void CylindricalLens_FocusesOneAxisOnly() /// 21-23 s for the same runs, so it gets 45 s. The budget is a regression guard against an order-of-magnitude slowdown, not a benchmark. public static double ScaleBudgetS() => Environment.GetEnvironmentVariable("CI") != null ? 45.0 : 13.0; - [Test(Slow = true, Reference = "S2 scale spike (docs/design/notes/spikes.md): a 1920 x 1080 single-frequency run (modulator + 0.2 m gap + beam profile) took 3.3 s and 1.05 GB peak on the owner's laptop; a Gaussian overfilling the panel passes the Gaussian power on the panel (erf in x and y)", Tolerance = "budget 4x the spike time (13 s; 45 s when the CI variable is set); keepProfiles used holds at least one profile less than all; transmission 1e-3")] + [Test(Slow = true, Reference = "S2 scale spike (docs/design/history.md): a 1920 x 1080 single-frequency run (modulator + 0.2 m gap + beam profile) took 3.3 s and 1.05 GB peak on the owner's laptop; a Gaussian overfilling the panel passes the Gaussian power on the panel (erf in x and y)", Tolerance = "budget 4x the spike time (13 s; 45 s when the CI variable is set); keepProfiles used holds at least one profile less than all; transmission 1e-3")] public void FullPanel_1920x1080_SingleFrequency_RunsWithinTheSpikeBudget() { int nx = 1920, ny = 1080; double pitch = 8e-6, dx = pitch, w = 6e-3; // the grid (2048 x 1280 cells of 8 um, one per device cell, aligned) is larger than the panel (15.4 x 8.6 mm) diff --git a/tests/UltrafastEngine.Tests/TestFramework.cs b/tests/UltrafastEngine.Tests/TestFramework.cs index 04e10b3..a7b1335 100644 --- a/tests/UltrafastEngine.Tests/TestFramework.cs +++ b/tests/UltrafastEngine.Tests/TestFramework.cs @@ -61,6 +61,8 @@ public static class Program { public static int Main(string[] args) { + // tests never read the owner's own material catalogue (~/.ultrafast/materials); the catalogue tests point at temporary folders + if (Environment.GetEnvironmentVariable("ULTRAFAST_MATERIALS") == null) Environment.SetEnvironmentVariable("ULTRAFAST_MATERIALS", ""); // the first-party plugins are referenced by this runner: register them in code (as a NativeAOT host would) UltrafastEngine.Plugins.PluginManager.Register(new UltrafastEngine.Plugins.Fitting.FittingPlugin()); UltrafastEngine.Plugins.PluginManager.Register(new UltrafastEngine.Zemax.ZemaxPlugin()); diff --git a/tests/UltrafastEngine.Tests/TileSourceTests.cs b/tests/UltrafastEngine.Tests/TileSourceTests.cs index 640d2cc..d0b51d7 100644 --- a/tests/UltrafastEngine.Tests/TileSourceTests.cs +++ b/tests/UltrafastEngine.Tests/TileSourceTests.cs @@ -71,6 +71,33 @@ public void PistonErrors_CostTheRealisedCoherenceSum() Assert.Close(1.0 / n + (1 - 1.0 / n) * Math.Exp(-sg * sg), acc / draws, 0.01, 0, "expectation"); } + [Test(Reference = "N = 4 coherent beams of equal power recombined at one point: a balanced tree of 50/50 splitters (a Mach-Zehnder of Mach-Zehnders) with piston phase phi_k in each arm returns |sum exp(i phi_k)|^2 / 16 of the input power at its bright port (all arms equal: the whole input), exactly; the tile source gives the same fraction of its in-phase on-axis fluence. A Branched system recombines at a point and so cannot give the array's far field: this is the on-axis value only", Tolerance = "Branched 1e-6 of the formula (the carrier is snapped to the grid), tiles against Branched 1.5 %")] + public void FourTiles_OnAxisPistonLoss_EqualsABranchedBalancedTreeRecombinedAtOnePoint() + { + double c = Numerics.PhysicalConstants.SpeedOfLightMPerS, w0 = 2 * Math.PI * c / Lam; + var phi = new[] { 0.0, 0.7, 2.1, -1.3 }; + JsonObject Bs(string id, JsonValue next) => O("id", id, "type", "beamSplitter", "reflectivity", 0.5, "next", next); + JsonObject Arm(string id, double ph, string to) => O("id", id, "type", "gap", "material", "Vacuum", "lengthM", 0.2 + (ph < 0 ? ph + 2 * Math.PI : ph) * c / w0, "next", to); + double Run(double[] ph, out double wc) + { + var sys = Sys(Bs("bs1", O("c", "bsA", "d", "bsB")), + Bs("bsA", O("c", "a0", "d", "a1")), Bs("bsB", O("c", "a2", "d", "a3")), + Arm("a0", ph[0], "bsC:1"), Arm("a1", ph[1], "bsC:2"), Arm("a2", ph[2], "bsD:1"), Arm("a3", ph[3], "bsD:2"), + Bs("bsC", O("c", "virtual", "d", "bsE:1")), Bs("bsD", O("c", "virtual", "d", "bsE:2")), Bs("bsE", O("c", "virtual", "d", "end"))); + sys.Topology = Systems.SystemGraph.Branched; + var r = Go(sys, Spec(O("shape", "cw", "centerWavelengthM", Lam, "powerW", 1.0))); + wc = r.Output.Grid.OmegaCenter; + return r.Output.TotalEnergyJ() / r.Compiled.Input.TotalEnergyJ(); + } + double inPhase = Run(new double[4], out _); + Assert.Close(1.0, inPhase, 1e-6, 0, "equal arms: the whole input at the bright port"); + double got = Run(phi, out double wcc); + double re = phi.Sum(p => Math.Cos((p < 0 ? p + 2 * Math.PI : p) * wcc / w0)), im = phi.Sum(p => Math.Sin((p < 0 ? p + 2 * Math.PI : p) * wcc / w0)), expect = (re * re + im * im) / 16; // the arm length of a negative phase is wrapped by 2 pi + Assert.Close(expect, got, 1e-6, 1e-9, "Branched tree against the formula"); + double tiles = OnAxis(Far(Tiles(4, "pistonRad", Nums(phi)))) / OnAxis(Far(Tiles(4))); + Assert.Close(got, tiles, 0.015, 0, "tile source against the Branched tree"); + } + [Test(Reference = "delay mismatch of two in-phase Gaussian pulses (intensity FWHM T): the energy on axis is 2E + 2 Int A(t) A(t - tau) dt, so relative to tau = 0 it is (1 + C) / 2 with the field overlap C = exp(-ln2 (tau / T)^2)", Tolerance = "3 %")] public void DelayMismatch_CostsTheTemporalOverlap_OfTheTwoPulses() { diff --git a/tests/components/media.py b/tests/components/media.py index 2fab6f0..89be60b 100644 --- a/tests/components/media.py +++ b/tests/components/media.py @@ -583,10 +583,10 @@ def adk_rate(i_wm2, ip_ev, l=1, m=0, z=1): return w * 4.134137333518e16 -def ion_cell(i0, lam, tau, model, p=1.0, L=1e-3, species="Ar", avalanche=False): +def ion_cell(i0, lam, tau, model, p=1.0, L=1e-3, species="Ar", avalanche=False, freq_resolved=True): area = 1e-7 mat = {"gas": b.gas(species, p, 293.15), "n2": 0.0} - el = b.bulk("cell", "m", L, beam=b.constant_beam(area_m2=area), ionization={"model": model, "avalanche": avalanche}) + el = b.bulk("cell", "m", L, beam=b.constant_beam(area_m2=area), ionization={"model": model, "avalanche": avalanche, "frequencyResolved": freq_resolved}) r = uf.run(b.system("c", [el], materials={"m": mat}), b.run_spec(b.pulse(lam, tau, energy=i0 * area * tau / GAUSS_P0), physics={"selfSteepening": False})) return r, area @@ -631,7 +631,8 @@ def ionisation(fam: Family): eng, ana, xs = [], [], [] for lam, i0 in ((2e-6, 0.5e18), (2e-6, 0.7e18), (1030e-9, 0.9e18)): tau = 30e-15 - r, area = ion_cell(i0, lam, tau, "adk") + # the reference formula is the single-carrier Drude phase: compare it with the carrier-frequency mode + r, area = ion_cell(i0, lam, tau, "adk", freq_resolved=False) P = np.asarray(r["input_power_w"]) t = np.asarray(r["input_time_s"]) dt = float(t[1] - t[0]) @@ -645,6 +646,11 @@ def ionisation(fam: Family): got = centroid(r["output_omega_rad_s"], r["output_spectrum"]) - centroid(r["input_omega_rad_s"], r["input_spectrum"]) fam.check(f"plasma blue shift {label_wl(lam)} I={i0 / 1e4:.1e} W/cm²", got, want, rel=0.05, unit="rad/s", formula="Δω = (k0 L / 2ρ_c) ⟨dρ/dt⟩ (Drude, τ_c ω0 >> 1); 5 %; thin enough that the ionisation loss (<1e-4) does not lower the intensity along the cell") + # frequency-resolved Drude (default): 1/omega across the bandwidth lowers the shift by a first-order amount in dw/w0 + rf, _ = ion_cell(i0, lam, tau, "adk") + gotf = centroid(rf["output_omega_rad_s"], rf["output_spectrum"]) - centroid(rf["input_omega_rad_s"], rf["input_spectrum"]) + fam.check(f"plasma blue shift, frequency-resolved Drude, {label_wl(lam)} I={i0 / 1e4:.1e} W/cm²", gotf, want, rel=0.08, unit="rad/s", + formula="same formula, 8 %: the 1/omega dependence of the Drude index across the 30 fs bandwidth lowers the shift by up to 5 % (measured -5.3 % at 1030 nm)") eng.append(got) ana.append(want) xs.append(f"{label_wl(lam)}\n{i0 / 1e4:.1e}")