From 98e7e2bdb0e697877ed140e44d787dd5ac24069a Mon Sep 17 00:00:00 2001 From: John Travers Date: Mon, 26 Oct 2026 19:18:34 +0000 Subject: [PATCH] Add mode_average=True to the capillary and waveguide interfaces prop_capillary, prop_waveguide and their Simulation constructors take mode_average=True to run their one gas-filled mode mode-averaged, as a uniform disc of its effective area: the one-node transform a Simulation without a transverse quadrature already took. The run is bitwise the hand-built one, and its default statistics are the capillary's own (plasma, species, profiles) less the quadrature estimate. More than one mode, n_r, n_theta and radial_rule are refused beside it, and so is the general modal route. The default stays the resolved single mode. The mode-averaging guide, the setup pages, the Luna mapping (whose modes=:HE11 is the mode-averaged run) and the MI ensemble and Raman comb examples use the switch; both examples were re-run and their figures are unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- ...al_instability_supercontinuum.codeobj.json | 202 +----------------- ...ulational_instability_supercontinuum.ipynb | 4 +- ...modulational_instability_supercontinuum.py | 23 +- ...lational_instability_supercontinuum.py.md5 | 2 +- ...odulational_instability_supercontinuum.rst | 43 ++-- ...odulational_instability_supercontinuum.zip | Bin 19683 -> 18954 bytes ..._04_raman_comb_supercontinuum.codeobj.json | 202 +----------------- .../plot_04_raman_comb_supercontinuum.ipynb | 4 +- .../plot_04_raman_comb_supercontinuum.py | 23 +- .../plot_04_raman_comb_supercontinuum.py.md5 | 2 +- .../plot_04_raman_comb_supercontinuum.rst | 43 ++-- .../plot_04_raman_comb_supercontinuum.zip | Bin 13309 -> 12588 bytes ...modulational_instability_supercontinuum.py | 23 +- .../plot_04_raman_comb_supercontinuum.py | 23 +- docs/guide/setup/model-selection.md | 5 +- docs/guide/setup/spatial-decompositions.md | 10 +- docs/guide/waveguides/mode-averaging.md | 42 ++-- docs/guide/workflows/luna.md | 2 +- docs/introduction/spatial-description.md | 2 +- src/solprop/driver/interface.py | 28 ++- src/solprop/driver/simulation.py | 161 ++++++++++++-- tests/driver/test_general_modal.py | 1 + tests/driver/test_mode_average.py | 149 +++++++++++++ 24 files changed, 424 insertions(+), 573 deletions(-) create mode 100644 tests/driver/test_mode_average.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e7fe99..88b5e778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ The first public release. It contains: the generalised nonlinear Schrödinger equation (GNLSE), for the real electric field or a complex envelope, in waveguides and in free space. - **Waveguides.** - - Gas-filled hollow capillaries, mode-averaged or with resolved modes. Tapers, + - Gas-filled hollow capillaries, with resolved modes (the default) or mode-averaged + (`mode_average=True` on `prop_capillary` and `prop_waveguide`). Tapers, pressure gradients and gas flow are supported, as is a coherent wall that couples the modes. - Antiresonant hollow-core fibres (the Vincetti model). diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.codeobj.json b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.codeobj.json index 9b940b36..e230b663 100644 --- a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.codeobj.json +++ b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.codeobj.json @@ -89,50 +89,6 @@ "name": "int" } ], - "Simulation.from_capillary": [ - { - "is_class": true, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.from_capillary" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.Simulation", - "module_short": "solprop.driver.Simulation", - "name": "from_capillary" - } - ], "ax": [ { "is_class": false, @@ -1009,22 +965,6 @@ "name": "ndarray" } ], - "default_spec": [ - { - "is_class": false, - "is_explicit": false, - "module": "builtins", - "module_short": "builtins", - "name": "function" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.stats", - "module_short": "solprop.stats", - "name": "default_spec" - } - ], "density": [ { "is_class": false, @@ -1535,7 +1475,7 @@ "name": "subplots" } ], - "replace": [ + "prop_capillary": [ { "is_class": false, "is_explicit": false, @@ -1546,9 +1486,9 @@ { "is_class": false, "is_explicit": false, - "module": "dataclasses", - "module_short": "dataclasses", - "name": "replace" + "module": "solprop.driver", + "module_short": "solprop.driver", + "name": "prop_capillary" } ], "rin": [ @@ -1767,140 +1707,6 @@ "name": "ndarray" } ], - "simulation": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation" - } - ], - "simulation.run": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.run" - } - ], - "simulation.settings": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.settings" - } - ], - "simulation.translate": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.translate" - } - ], "spectra": [ { "is_class": false, diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.ipynb b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.ipynb index 35d23d57..e22b826d 100644 --- a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.ipynb +++ b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## The ensemble\nEach shot is the same run with a different realisation of the quantum shot noise:\none photon per active spectral bin, with a random phase, added to the input spectrum\n(``noise=\"input\"``), drawn from the generator seeded by ``seed``.\nClearing the transverse quadrature selects mode averaging. Merely setting\n``modes=1`` would retain a resolved single-mode calculation. The assertions check\nboth the translated problem and the saved metadata so the approximation is\nrecorded with the result.\n\n" + "## The ensemble\nEach shot is the same run with a different realisation of the quantum shot noise:\none photon per active spectral bin, with a random phase, added to the input spectrum\n(``noise=\"input\"``), drawn from the generator seeded by ``seed``.\n``mode_average=True`` selects mode averaging; ``modes=1`` alone, the default, would\nretain a resolved single-mode calculation. The assertions check the saved metadata\nso the approximation is recorded with the result.\n\n" ] }, { @@ -22,7 +22,7 @@ }, "outputs": [], "source": [ - "import time\nimport warnings\nfrom dataclasses import replace\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nfrom solprop.constants import C\nfrom solprop.driver import Simulation\nfrom solprop.exceptions import MaterialRangeWarning\nfrom solprop.processing import g12_of, get_spectral_density\nfrom solprop.stats import default_spec\n\nSHOTS = 8\n\"\"\"The size of the ensemble.\"\"\"\n\nstarted = time.perf_counter()\nruns = []\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", MaterialRangeWarning)\n for seed in range(SHOTS):\n simulation = Simulation.from_capillary(\n 16e-6,\n 0.4,\n \"Ar\",\n 40.0,\n lambda0=1030e-9,\n fwhm=400e-15,\n energy=5e-6,\n loss=False,\n plasma=True,\n raman=False,\n trange=4e-12,\n wavelength_limits=(170e-9, 4e-6),\n noise=\"input\",\n seed=seed,\n n_save=101,\n )\n simulation.settings = replace(\n simulation.settings,\n quadrature=None,\n components=None,\n stats=default_spec(\"mode_averaged\"),\n )\n assert simulation.translate().flags.mode_averaged\n run = simulation.run(progress=\"none\")\n assert run.metadata.simulation_kind == \"mode_averaged\"\n assert run.metadata.quadrature is None\n runs.append(run)\n print(f\"seed {seed}: {run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m\")" + "import time\nimport warnings\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nfrom solprop.constants import C\nfrom solprop.driver import prop_capillary\nfrom solprop.exceptions import MaterialRangeWarning\nfrom solprop.processing import g12_of, get_spectral_density\n\nSHOTS = 8\n\"\"\"The size of the ensemble.\"\"\"\n\nstarted = time.perf_counter()\nruns = []\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", MaterialRangeWarning)\n for seed in range(SHOTS):\n run = prop_capillary(\n 16e-6,\n 0.4,\n \"Ar\",\n 40.0,\n lambda0=1030e-9,\n fwhm=400e-15,\n energy=5e-6,\n loss=False,\n plasma=True,\n raman=False,\n trange=4e-12,\n wavelength_limits=(170e-9, 4e-6),\n noise=\"input\",\n seed=seed,\n n_save=101,\n mode_average=True,\n progress=\"none\",\n )\n assert run.metadata.simulation_kind == \"mode_averaged\"\n assert run.metadata.quadrature is None\n runs.append(run)\n print(f\"seed {seed}: {run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m\")" ] }, { diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py index 42ff3545..c32e78cf 100644 --- a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py +++ b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py @@ -53,23 +53,20 @@ # Each shot is the same run with a different realisation of the quantum shot noise: # one photon per active spectral bin, with a random phase, added to the input spectrum # (``noise="input"``), drawn from the generator seeded by ``seed``. -# Clearing the transverse quadrature selects mode averaging. Merely setting -# ``modes=1`` would retain a resolved single-mode calculation. The assertions check -# both the translated problem and the saved metadata so the approximation is -# recorded with the result. +# ``mode_average=True`` selects mode averaging; ``modes=1`` alone, the default, would +# retain a resolved single-mode calculation. The assertions check the saved metadata +# so the approximation is recorded with the result. import time import warnings -from dataclasses import replace import matplotlib.pyplot as plt import numpy as np from solprop.constants import C -from solprop.driver import Simulation +from solprop.driver import prop_capillary from solprop.exceptions import MaterialRangeWarning from solprop.processing import g12_of, get_spectral_density -from solprop.stats import default_spec SHOTS = 8 """The size of the ensemble.""" @@ -79,7 +76,7 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore", MaterialRangeWarning) for seed in range(SHOTS): - simulation = Simulation.from_capillary( + run = prop_capillary( 16e-6, 0.4, "Ar", @@ -95,15 +92,9 @@ noise="input", seed=seed, n_save=101, + mode_average=True, + progress="none", ) - simulation.settings = replace( - simulation.settings, - quadrature=None, - components=None, - stats=default_spec("mode_averaged"), - ) - assert simulation.translate().flags.mode_averaged - run = simulation.run(progress="none") assert run.metadata.simulation_kind == "mode_averaged" assert run.metadata.quadrature is None runs.append(run) diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py.md5 b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py.md5 index 71ef4ba7..8b82baae 100644 --- a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py.md5 +++ b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.py.md5 @@ -1 +1 @@ -814fb0daa5794e3950a30c26755b6a26 \ No newline at end of file +4bfd22e913423f4b067115cb97c2aca8 \ No newline at end of file diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.rst b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.rst index 4dc2dbc2..31f33b3a 100644 --- a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.rst +++ b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.rst @@ -61,35 +61,32 @@ for the noise convention and estimator see :doc:`/guide/media/noise` and :doc:`/introduction/lessons/plot_01_rdw` follows a shorter pulse whose broadening is organised by self-compression and dispersive-wave emission. -.. GENERATED FROM PYTHON SOURCE LINES 51-60 +.. GENERATED FROM PYTHON SOURCE LINES 51-59 The ensemble ------------ Each shot is the same run with a different realisation of the quantum shot noise: one photon per active spectral bin, with a random phase, added to the input spectrum (``noise="input"``), drawn from the generator seeded by ``seed``. -Clearing the transverse quadrature selects mode averaging. Merely setting -``modes=1`` would retain a resolved single-mode calculation. The assertions check -both the translated problem and the saved metadata so the approximation is -recorded with the result. +``mode_average=True`` selects mode averaging; ``modes=1`` alone, the default, would +retain a resolved single-mode calculation. The assertions check the saved metadata +so the approximation is recorded with the result. -.. GENERATED FROM PYTHON SOURCE LINES 60-112 +.. GENERATED FROM PYTHON SOURCE LINES 59-103 .. code-block:: Python import time import warnings - from dataclasses import replace import matplotlib.pyplot as plt import numpy as np from solprop.constants import C - from solprop.driver import Simulation + from solprop.driver import prop_capillary from solprop.exceptions import MaterialRangeWarning from solprop.processing import g12_of, get_spectral_density - from solprop.stats import default_spec SHOTS = 8 """The size of the ensemble.""" @@ -99,7 +96,7 @@ recorded with the result. with warnings.catch_warnings(): warnings.simplefilter("ignore", MaterialRangeWarning) for seed in range(SHOTS): - simulation = Simulation.from_capillary( + run = prop_capillary( 16e-6, 0.4, "Ar", @@ -115,15 +112,9 @@ recorded with the result. noise="input", seed=seed, n_save=101, + mode_average=True, + progress="none", ) - simulation.settings = replace( - simulation.settings, - quadrature=None, - components=None, - stats=default_spec("mode_averaged"), - ) - assert simulation.translate().flags.mode_averaged - run = simulation.run(progress="none") assert run.metadata.simulation_kind == "mode_averaged" assert run.metadata.quadrature is None runs.append(run) @@ -149,13 +140,13 @@ recorded with the result. -.. GENERATED FROM PYTHON SOURCE LINES 113-116 +.. GENERATED FROM PYTHON SOURCE LINES 104-107 The averaged spectrum along the fibre ------------------------------------- The spectral energy density averaged over the ensemble, in decibels below its peak. -.. GENERATED FROM PYTHON SOURCE LINES 116-145 +.. GENERATED FROM PYTHON SOURCE LINES 107-136 .. code-block:: Python @@ -200,7 +191,7 @@ The spectral energy density averaged over the ensemble, in decibels below its pe -.. GENERATED FROM PYTHON SOURCE LINES 146-153 +.. GENERATED FROM PYTHON SOURCE LINES 137-144 Noise and coherence at the exit ------------------------------- @@ -210,7 +201,7 @@ its mean, shot to shot; and the first-order coherence :math:`|g_{12}^{(1)}(\lambda)|` of the ensemble's complex spectra (0 for independent fields, 1 for identical ones). -.. GENERATED FROM PYTHON SOURCE LINES 153-192 +.. GENERATED FROM PYTHON SOURCE LINES 144-183 .. code-block:: Python @@ -265,7 +256,7 @@ independent fields, 1 for identical ones). -.. GENERATED FROM PYTHON SOURCE LINES 193-205 +.. GENERATED FROM PYTHON SOURCE LINES 184-196 In this 0.4 m calculation, broadening becomes pronounced around 0.2 m on the displayed 40 dB scale. The exit spectrum spans the visible and near infrared, @@ -280,7 +271,7 @@ Repeat with more seeds before using these curves as quantitative ensemble statistics. Check temporal sampling and band limits independently of the propagation tolerance; see :doc:`/guide/setup/grids`. -.. GENERATED FROM PYTHON SOURCE LINES 205-210 +.. GENERATED FROM PYTHON SOURCE LINES 196-201 .. code-block:: Python @@ -297,7 +288,7 @@ propagation tolerance; see :doc:`/guide/setup/grids`. .. code-block:: none - 620 s for 8 shots; backend native + 716 s for 8 shots; backend native @@ -305,7 +296,7 @@ propagation tolerance; see :doc:`/guide/setup/grids`. .. rst-class:: sphx-glr-timing - **Total running time of the script:** (10 minutes 19.772 seconds) + **Total running time of the script:** (11 minutes 56.193 seconds) .. _sphx_glr_download_auto_examples_supercontinuum_plot_03_modulational_instability_supercontinuum.py: diff --git a/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.zip b/docs/auto_examples/supercontinuum/plot_03_modulational_instability_supercontinuum.zip index 41f0227a77bd2da80fbea21e9002f0c8c5f76f8d..a1f514f2748592b8499fb359f64c7b85706349e2 100644 GIT binary patch delta 640 zcmaDnld)?GqeOr=Gm8iV2-wbuihVS-z))0?fk9MpqeKi(XhK46eoAV5Vp(cYVtT4= zNKt8OLV`kZYEEi$NwES*P5~m9nU`*@08w3RYY0@6n3JEEs-sYnk*biAnwD6aQ=+3# zo?n`i!lgX+hdXejCf69;o?KyA%7+>>lfN65OrC7?6ggC;7|&re z1$iAaRATaAK?0)T!4d-wml&nVuS{hpR|qL=o@~}E%7>a-Cad@aDPV*YI9Wl6C5@?y R6J?AhpYRc7GxP+B0Ra0n)rtTB delta 1323 zcmb`H&ubGw6vsCzf+1TYsE9w>$p-2M%CN- z!$(dU`Z-;%)?Qx!GSjsTL%)~g!$SFWWja^8h7n|K(gI=w0l^^QcM#(UbRy_8C?bXc zM;J8(2LbgFfJ%lf72trIh#`DW$wH|AY!TG!+QQxWdL7)Q5%%S~+|@-TtW&X(Y=EIa zJ_s3I$0z`h__4%QW*-DdKpzSyC(Sd5vPJp9IV)Gq$*Crz0nkDNL(UNg?I5H~0ER*g z8%PFbCtGa?k;>hsM1D0p({Tu=SZx$KTtL;qAjn5e7-6x-L)4Jo#HaIo7ztdEq;YhL z-qP{Mnaxrb44%a63_9m3Xi$eev}yMk>rBd!4B4$vg3@Axsul?%g1eQ?-_*DvceE4R z($in-9}x1bQj#vIUJp-=ZKSgJ%=MAjC8vpDi#w?_{dzB|ZdgQAqa%@O!B)d=f_qv7cocG^2^T=4t7CiEglzBvcboPn|5ato(qvn*{ znZ2E>4tY_F-^{XnFn8r|d3!szvp;XI=ASFxJTl;IbmC)xOMHJKF75agxwQI|xg`yq zW-cv{fzC@)=UFA$EnldOU~&Lp$qxGi=Ea1m{A%Qx6SKWH=Eccf4N3%Ed;t%%2zJ34z diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.codeobj.json b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.codeobj.json index 833f710d..f69366f0 100644 --- a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.codeobj.json +++ b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.codeobj.json @@ -64,50 +64,6 @@ "name": "MaterialRangeWarning" } ], - "Simulation.from_capillary": [ - { - "is_class": true, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.from_capillary" - }, - { - "is_class": true, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.from_capillary" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.Simulation", - "module_short": "solprop.driver.Simulation", - "name": "from_capillary" - } - ], "ax": [ { "is_class": false, @@ -189,22 +145,6 @@ "name": "Artist.set_title" } ], - "default_spec": [ - { - "is_class": false, - "is_explicit": false, - "module": "builtins", - "module_short": "builtins", - "name": "function" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.stats", - "module_short": "solprop.stats", - "name": "default_spec" - } - ], "fig": [ { "is_class": false, @@ -306,7 +246,7 @@ "name": "prop_2d" } ], - "replace": [ + "prop_capillary": [ { "is_class": false, "is_explicit": false, @@ -317,9 +257,9 @@ { "is_class": false, "is_explicit": false, - "module": "dataclasses", - "module_short": "dataclasses", - "name": "replace" + "module": "solprop.driver", + "module_short": "solprop.driver", + "name": "prop_capillary" } ], "run": [ @@ -540,140 +480,6 @@ "name": "ABC.metadata" } ], - "simulation": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation" - } - ], - "simulation.run": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.run" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.run" - } - ], - "simulation.settings": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.settings" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.settings" - } - ], - "simulation.translate": [ - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver.simulation", - "module_short": "solprop.driver", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop.driver", - "module_short": "solprop.driver", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Simulation.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop._display", - "module_short": "solprop._display", - "name": "Displayed.translate" - }, - { - "is_class": false, - "is_explicit": false, - "module": "solprop", - "module_short": "solprop", - "name": "Displayed.translate" - } - ], "spec_1d": [ { "is_class": false, diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.ipynb b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.ipynb index 794ebfe5..e8354384 100644 --- a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.ipynb +++ b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set up and run the model\n``Simulation.from_capillary`` assembles the material and pulse choices. We then\nremove its transverse quadrature and request the mode-averaged statistics before\ntranslating or running. ``modes=1`` on its own would select a resolved single\nmode, which is a different nonlinear approximation.\n\n" + "## Set up and run the model\n``prop_capillary`` assembles the material and pulse choices, and\n``mode_average=True`` selects mode averaging. ``modes=1`` on its own, the default,\nwould select a resolved single mode, which is a different nonlinear approximation.\n\n" ] }, { @@ -22,7 +22,7 @@ }, "outputs": [], "source": [ - "import time\nimport warnings\nfrom dataclasses import replace\n\nimport matplotlib.pyplot as plt\n\nfrom solprop.driver import Simulation\nfrom solprop.exceptions import MaterialRangeWarning\nfrom solprop.plotting import prop_2d, spec_1d\nfrom solprop.stats import default_spec\n\nstarted = time.perf_counter()\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", MaterialRangeWarning)\n simulation = Simulation.from_capillary(\n 16e-6,\n 0.4,\n \"H2\",\n 40.0,\n lambda0=515e-9,\n fwhm=400e-15,\n energy=10e-6,\n loss=False,\n plasma=True,\n raman=True,\n trange=4e-12,\n wavelength_limits=(170e-9, 4e-6),\n noise=\"input\",\n seed=0,\n n_save=201,\n )\n simulation.settings = replace(\n simulation.settings,\n quadrature=None,\n components=None,\n stats=default_spec(\"mode_averaged\"),\n )\n assert simulation.translate().flags.mode_averaged\n run = simulation.run(progress=\"none\")\nassert run.metadata.simulation_kind == \"mode_averaged\"\nassert run.metadata.quadrature is None\nprint(f\"{run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m; seed 0\")" + "import time\nimport warnings\n\nimport matplotlib.pyplot as plt\n\nfrom solprop.driver import prop_capillary\nfrom solprop.exceptions import MaterialRangeWarning\nfrom solprop.plotting import prop_2d, spec_1d\n\nstarted = time.perf_counter()\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", MaterialRangeWarning)\n run = prop_capillary(\n 16e-6,\n 0.4,\n \"H2\",\n 40.0,\n lambda0=515e-9,\n fwhm=400e-15,\n energy=10e-6,\n loss=False,\n plasma=True,\n raman=True,\n trange=4e-12,\n wavelength_limits=(170e-9, 4e-6),\n noise=\"input\",\n seed=0,\n n_save=201,\n mode_average=True,\n progress=\"none\",\n )\nassert run.metadata.simulation_kind == \"mode_averaged\"\nassert run.metadata.quadrature is None\nprint(f\"{run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m; seed 0\")" ] }, { diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py index 58d51cc9..05346086 100644 --- a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py +++ b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py @@ -47,26 +47,23 @@ # %% # Set up and run the model # ------------------------ -# ``Simulation.from_capillary`` assembles the material and pulse choices. We then -# remove its transverse quadrature and request the mode-averaged statistics before -# translating or running. ``modes=1`` on its own would select a resolved single -# mode, which is a different nonlinear approximation. +# ``prop_capillary`` assembles the material and pulse choices, and +# ``mode_average=True`` selects mode averaging. ``modes=1`` on its own, the default, +# would select a resolved single mode, which is a different nonlinear approximation. import time import warnings -from dataclasses import replace import matplotlib.pyplot as plt -from solprop.driver import Simulation +from solprop.driver import prop_capillary from solprop.exceptions import MaterialRangeWarning from solprop.plotting import prop_2d, spec_1d -from solprop.stats import default_spec started = time.perf_counter() with warnings.catch_warnings(): warnings.simplefilter("ignore", MaterialRangeWarning) - simulation = Simulation.from_capillary( + run = prop_capillary( 16e-6, 0.4, "H2", @@ -82,15 +79,9 @@ noise="input", seed=0, n_save=201, + mode_average=True, + progress="none", ) - simulation.settings = replace( - simulation.settings, - quadrature=None, - components=None, - stats=default_spec("mode_averaged"), - ) - assert simulation.translate().flags.mode_averaged - run = simulation.run(progress="none") assert run.metadata.simulation_kind == "mode_averaged" assert run.metadata.quadrature is None print(f"{run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m; seed 0") diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py.md5 b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py.md5 index a61fe9b4..dbdbc1a2 100644 --- a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py.md5 +++ b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.py.md5 @@ -1 +1 @@ -f05bd3790bff5912bcdffdcdbfbaa383 \ No newline at end of file +c8c1e620bfe7c3f0b549aa9ec0829db5 \ No newline at end of file diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.rst b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.rst index 41ef2e23..5c296f42 100644 --- a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.rst +++ b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.rst @@ -58,35 +58,32 @@ nitrogen-filled hollow-core anti-resonant fiber `_; its gas and pump conditions differ from this hydrogen calculation. -.. GENERATED FROM PYTHON SOURCE LINES 48-54 +.. GENERATED FROM PYTHON SOURCE LINES 48-53 Set up and run the model ------------------------ -``Simulation.from_capillary`` assembles the material and pulse choices. We then -remove its transverse quadrature and request the mode-averaged statistics before -translating or running. ``modes=1`` on its own would select a resolved single -mode, which is a different nonlinear approximation. +``prop_capillary`` assembles the material and pulse choices, and +``mode_average=True`` selects mode averaging. ``modes=1`` on its own, the default, +would select a resolved single mode, which is a different nonlinear approximation. -.. GENERATED FROM PYTHON SOURCE LINES 54-98 +.. GENERATED FROM PYTHON SOURCE LINES 53-89 .. code-block:: Python import time import warnings - from dataclasses import replace import matplotlib.pyplot as plt - from solprop.driver import Simulation + from solprop.driver import prop_capillary from solprop.exceptions import MaterialRangeWarning from solprop.plotting import prop_2d, spec_1d - from solprop.stats import default_spec started = time.perf_counter() with warnings.catch_warnings(): warnings.simplefilter("ignore", MaterialRangeWarning) - simulation = Simulation.from_capillary( + run = prop_capillary( 16e-6, 0.4, "H2", @@ -102,15 +99,9 @@ mode, which is a different nonlinear approximation. noise="input", seed=0, n_save=201, + mode_average=True, + progress="none", ) - simulation.settings = replace( - simulation.settings, - quadrature=None, - components=None, - stats=default_spec("mode_averaged"), - ) - assert simulation.translate().flags.mode_averaged - run = simulation.run(progress="none") assert run.metadata.simulation_kind == "mode_averaged" assert run.metadata.quadrature is None print(f"{run.metadata.simulation_kind}, z = {run['z'][-1]:.2f} m; seed 0") @@ -128,12 +119,12 @@ mode, which is a different nonlinear approximation. -.. GENERATED FROM PYTHON SOURCE LINES 99-101 +.. GENERATED FROM PYTHON SOURCE LINES 90-92 Spectral and temporal evolution ------------------------------- -.. GENERATED FROM PYTHON SOURCE LINES 101-115 +.. GENERATED FROM PYTHON SOURCE LINES 92-106 .. code-block:: Python @@ -163,14 +154,14 @@ Spectral and temporal evolution -.. GENERATED FROM PYTHON SOURCE LINES 116-120 +.. GENERATED FROM PYTHON SOURCE LINES 107-111 The comb and the continuum -------------------------- The four positions lie within the 0.4 m calculation. Compare peak locations as well as their broadening: a logarithmic plot can make weak features prominent. -.. GENERATED FROM PYTHON SOURCE LINES 120-129 +.. GENERATED FROM PYTHON SOURCE LINES 111-120 .. code-block:: Python @@ -201,7 +192,7 @@ well as their broadening: a logarithmic plot can make weak features prominent. -.. GENERATED FROM PYTHON SOURCE LINES 130-141 +.. GENERATED FROM PYTHON SOURCE LINES 121-132 The early slices retain conspicuous discrete peaks. By 0.4 m, broadened components overlap across much of the visible and near infrared, with weaker @@ -215,7 +206,7 @@ A quantitative noise study needs repeated independent seeds, as in the MI ensemble. For sampling and material checks see :doc:`/guide/setup/grids` and :doc:`/guide/media/materials`. -.. GENERATED FROM PYTHON SOURCE LINES 141-143 +.. GENERATED FROM PYTHON SOURCE LINES 132-134 .. code-block:: Python @@ -229,7 +220,7 @@ ensemble. For sampling and material checks see :doc:`/guide/setup/grids` and .. code-block:: none - 202 s; backend native + 152 s; backend native @@ -237,7 +228,7 @@ ensemble. For sampling and material checks see :doc:`/guide/setup/grids` and .. rst-class:: sphx-glr-timing - **Total running time of the script:** (3 minutes 22.033 seconds) + **Total running time of the script:** (2 minutes 32.458 seconds) .. _sphx_glr_download_auto_examples_supercontinuum_plot_04_raman_comb_supercontinuum.py: diff --git a/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.zip b/docs/auto_examples/supercontinuum/plot_04_raman_comb_supercontinuum.zip index d18dbba742049a299a8dfbe52415a8b1952fa410..0ad68e14c1db24c170f5cbeae4685871d1b82950 100644 GIT binary patch delta 584 zcmeyHz9vZ~z?+#xgaHJ0&y0%Q)yNa|3djXv6^6DMTW<0LZd-nkz46J31(`WH ziA9xMlUFMnPmbo%pS+Q$maC{VPr+6Jq++uduM4BC0uW$0N{0(13<5xf=|!o<#kNX$ z`FW{I5D87L#Ny)Aq7sG4>(pg7@8EyLCd4}{DmIz-n$JHe1_lsT2Ze&>=Br}ah+v2Y z2SZFA^+O>hZ?b@r9HYW!10_Dj$??h#e5gS%d8KmNWOJ2fWZz#?iD0w2JHVBB^`*|W_xvB9zN8lpB!cEqJS9=;6wr@mNe>!0z<`;E#Cmd2LN%iyX61? delta 1294 zcmb`HJ#W)c6o$=*sN{qozA{DX!Hpo1S~pR}03xVD-H-}u2N;a%Tl*wLb?!~>y)I0I z4h(EamRML36HAqWogMxF690gofW)~@6DKL5AYsW-;&a~P_q=)RkGnrxrHA*=oV{Y1 zqv&rx*?Rl@_GZzt%vrXQAGPw~`W^eicDoZtOt%Ghn^JU=m-fxR3y%==Q{?s%NbHDx zgtk_oCF5!zr9$Y%ER;;g5`poMntCxRZ8XpkA+9iLV=W9pX_~EcJ5aETO(Bs+rgO|a zc?*OD8FNlwUGROQRgFU3_d)RVp5hr#F6I_b+e=9;x3y?2LdY}=BBm57Xm`3oYBJD` zSb)hv?yj9}lzhnr*HeiSQCEttYwV0gAvReY=^fR@AX&}7D5-H4(12`A8qDBRe&bZX zY6BfvpQ})tJroI!JXF$_Ic>}1VwPZIQ*g`*fuNN*a;;_xW%&l+GRpS7b=upOQKQ>4TqCAXRumWSh!@Uay}du z_DYsz&KriR>Y#UZ?*yzo{=cd6_-MCzP}TkXF{nJA)N^aeXZUs^Snsag%V$K8@GA+y zssAlr51dzj#_PNDnDFxW9B?N18urZTTh6Mpgk_ak#731qp=})Hk!r!Rfgr62DP-V}+&Tj$o-% None: + """Check a mode-averaged hollow run and return its absent transverse rule. + + A mode-averaged run carries one mode as a uniform disc of its effective area, the + one-node transform (:func:`solprop.translate.modal.mode_averaged_transform`), so it + has no rule for the keywords that shape one to act on. + + Parameters + ---------- + modes : solprop.modes.collection.ModeCollection + The mode set, which must hold exactly one mode. + n_r : int or None + Radial nodes, which must not be given. + n_theta : int or None + Azimuthal nodes, which must not be given. + radial_rule : str + The radial rule family, which must be the default. + + Raises + ------ + DriverError + If the set has more than one mode or a rule keyword is given. + """ + if len(modes) != 1: + msg = ( + f"mode_average=True needs exactly one mode, and the set has {len(modes)}: " + "energy moving between modes is what a mode-averaged run cannot carry " + "(docs/guide/waveguides/mode-averaging.md)" + ) + raise DriverError(msg) + given = [ + name + for name, value, unset in ( + ("n_r", n_r, None), + ("n_theta", n_theta, None), + ("radial_rule", radial_rule, "gauss_kronrod"), + ) + if value != unset + ] + if given: + msg = ( + f"{', '.join(given)} given beside mode_average=True: they shape a " + "transverse rule, and a mode-averaged run has none, its mode being a " + "uniform disc of the effective area" + ) + raise DriverError(msg) + + def _capillary_responses( # noqa: PLR0913 - the switches of the responses, one each gas: Gas | str, material: Species, @@ -8630,7 +8730,9 @@ def _warn_tdse_cost(quadrature: TransverseQuadrature | None) -> None: ) -def _capillary_stats(*, with_plasma: bool, mixture: Mixture) -> StatsSpec: +def _capillary_stats( + *, with_plasma: bool, mixture: Mixture, averaged: bool = False +) -> StatsSpec: """Return the statistics a capillary run records unless told otherwise. The multimode defaults with the z-profiles, plus every statistic only a backend can @@ -8645,6 +8747,10 @@ def _capillary_stats(*, with_plasma: bool, mixture: Mixture) -> StatsSpec: mixture : Mixture Ordered populations whose species statistics are included. Independent density paths have no total-pressure statistic. + averaged : bool, optional + Whether the run is mode-averaged: the mode-averaged defaults then replace the + multimode ones, and the quadrature estimate, which has no rule to read, is + left out. Returns ------- @@ -8654,9 +8760,12 @@ def _capillary_stats(*, with_plasma: bool, mixture: Mixture) -> StatsSpec: wanted = [ name for name in BACKEND_STATISTICS - if with_plasma or name not in PLASMA_STATISTICS + if (with_plasma or name not in PLASMA_STATISTICS) + and not (averaged and name == "quadrature_error") ] - base = default_spec("multimode", with_profiles=True) + base = default_spec( + "mode_averaged" if averaged else "multimode", with_profiles=True + ) wanted.extend( ( "species_ionisation", @@ -8686,6 +8795,7 @@ def _capillary_stats(*, with_plasma: bool, mixture: Mixture) -> StatsSpec: "preionisation": None, "plasma_integration": None, "raman": None, + "mode_average": False, "n_r": None, "n_theta": None, "radial_rule": "gauss_kronrod", @@ -8722,6 +8832,8 @@ def _capillary_stats(*, with_plasma: bool, mixture: Mixture) -> StatsSpec: "plasma_integration": "a general guide carries no plasma column to integrate", "raman": "a guide's delayed response is its material regions' own, the silica " "tensor Raman model", + "mode_average": "a general guide's channels are resolved on its own node rule; " + "mode averaging is the gas-filled route's", "n_r": "the transverse rule is the guide's own node rule, declared when the " "guide was built", "n_theta": "the transverse rule is the guide's own node rule, declared when " @@ -9191,7 +9303,7 @@ def _capillary_input_peak( field0: Complex[np.ndarray, "n_modes n_pol n_omega"], modes: ModeCollection, grid: TimeGrid, - rule: TransverseQuadrature, + rule: TransverseQuadrature | None, ) -> tuple[float, float]: """Return the input's peak field, which sets the ionisation table's range. @@ -9212,8 +9324,9 @@ def _capillary_input_peak( The mode set, whose profiles are evaluated on the axis and at the nodes. grid : solprop.grid.TimeGrid The grid the state is stored on. - rule : solprop.modes.quadrature.TransverseQuadrature - The run's transverse quadrature. + rule : solprop.modes.quadrature.TransverseQuadrature or None + The run's transverse quadrature; ``None`` for a mode-averaged run, whose + table the axis alone bounds: the area-mean field it samples lies below it. Returns ------- @@ -9225,7 +9338,9 @@ def _capillary_input_peak( limits = modes.dimlimits()[0] peak_power = 0.0 circular = 0.0 - for where in (on_axis_rule(limits), rule): + for where in ( + (on_axis_rule(limits),) if rule is None else (on_axis_rule(limits), rule) + ): matrices = mode_matrix(modes.modes, where, 0.0, components=("x", "y")) rows = matrices.synthesis.reshape(matrices.n_nodes, matrices.n_pol, -1) # One node at a time: the field at every node and time together would be @@ -9258,14 +9373,14 @@ class _InputPeak: The mode set. grid : solprop.grid.TimeGrid The grid the state is stored on. - rule : solprop.modes.quadrature.TransverseQuadrature - The run's transverse quadrature. + rule : solprop.modes.quadrature.TransverseQuadrature or None + The run's transverse quadrature; ``None`` for a mode-averaged run. """ field0: Complex[np.ndarray, "n_modes n_pol n_omega"] modes: ModeCollection grid: TimeGrid - rule: TransverseQuadrature + rule: TransverseQuadrature | None def evaluate(self) -> tuple[float, float]: """Return the peak field in V/m and its circular share. @@ -9644,7 +9759,7 @@ def _hollow_stats( with_plasma: bool, mixture: Mixture, profiles: Mapping[str, ZProfile], - rule: TransverseQuadrature, + rule: TransverseQuadrature | None, ) -> StatsSpec: """Keep capillary statistics where the supplied geometry defines them. @@ -9656,18 +9771,20 @@ def _hollow_stats( Ordered material populations. profiles : mapping of str to ZProfile Available physical paths. - rule : solprop.modes.quadrature.TransverseQuadrature + rule : solprop.modes.quadrature.TransverseQuadrature or None The run's fixed transverse rule, whose embedded coarse rule the - quadrature alarm needs. + quadrature alarm needs; ``None`` for a mode-averaged run. Returns ------- StatsSpec Statistics with unsupported geometry paths omitted. """ - base = _capillary_stats(with_plasma=with_plasma, mixture=mixture) + base = _capillary_stats( + with_plasma=with_plasma, mixture=mixture, averaged=rule is None + ) dropped = {name for name in ("core_radius",) if name not in profiles} - if not rule.kronrod: + if rule is not None and not rule.kronrod: # Without an embedded coarse rule the estimate is NaN at every plane, so # the statistic is dropped rather than recorded empty. A Cartesian # product rule has no Kronrod extension at all (quadrature_rule stores diff --git a/tests/driver/test_general_modal.py b/tests/driver/test_general_modal.py index f29f72a5..02f3f0bb 100644 --- a/tests/driver/test_general_modal.py +++ b/tests/driver/test_general_modal.py @@ -203,6 +203,7 @@ def test_a_provider_builds_a_simulation_that_translates_to_the_general_route( "preionisation": 0.1, "plasma_integration": "band", "raman": True, + "mode_average": True, "n_r": 16, "n_theta": 9, "radial_rule": "gauss", diff --git a/tests/driver/test_mode_average.py b/tests/driver/test_mode_average.py new file mode 100644 index 00000000..ef179f82 --- /dev/null +++ b/tests/driver/test_mode_average.py @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: 2026 John C. Travers and contributors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""`mode_average=True` runs a hollow guide's one mode mode-averaged. + +The switch is the one-node transform of the multimode pipeline, the route a +`Simulation` with no transverse quadrature already took: the capillary's run under it +is bitwise the run with the quadrature removed by hand. What it adds is the capillary's +own statistics, plasma and species included, and refusals for the keywords that shape +a transverse rule it does not have. +""" + +from dataclasses import replace + +import numpy as np +import pytest + +from solprop.driver import Simulation, prop_capillary, prop_waveguide +from solprop.exceptions import DriverError +from solprop.modes import MarcatiliMode, TubeLattice +from solprop.profiles import Linear +from solprop.stats import default_spec + +RADIUS = 100e-6 +"""Core radius, m.""" + +LENGTH = 5e-3 +"""Capillary length, m: a few adaptive steps.""" + +# The helium index is extrapolated beyond its fitted range at the grid's ends; that +# is the material model, not the switch under test. +pytestmark = pytest.mark.filterwarnings( + "ignore::solprop.exceptions.MaterialRangeWarning" +) + + +def _keywords(**overrides): + """Return a short, ionising helium capillary's keywords.""" + keywords = { + "lambda0": 800e-9, + "wavelength_limits": (150e-9, 4e-6), + "trange": 200e-15, + "fwhm": 30e-15, + "energy": 200e-6, + "n_save": 2, + "backend": "numpy_ref", + "progress": "none", + } + return keywords | overrides + + +@pytest.mark.medium +def test_the_switch_is_the_run_without_the_quadrature(): + """The keyword's run is bitwise the hand-built averaged run, with more stats.""" + keywords = _keywords() + progress = keywords.pop("progress") + by_hand = Simulation.from_capillary(RADIUS, LENGTH, "He", 2.0, **keywords) + by_hand.settings = replace( + by_hand.settings, + quadrature=None, + components=None, + stats=default_spec("mode_averaged"), + ) + expected = by_hand.run(progress=progress) + output = prop_capillary(RADIUS, LENGTH, "He", 2.0, **_keywords(), mode_average=True) + assert output.metadata is not None + assert output.metadata.simulation_kind == "mode_averaged" + assert output.metadata.quadrature is None + np.testing.assert_array_equal( + np.asarray(output["field"]), np.asarray(expected["field"]) + ) + names = set(output.stat_names) + assert { + "electron_density", + "peak_ionisation_rate", + "peak_intensity", + "species_electron_density", + "density", + "pressure", + "core_radius", + } <= names + assert "quadrature_error" not in names + assert "peak_power_total" not in names + + +def test_the_default_stays_resolved(): + """Without the switch, one mode is resolved on the radial rule.""" + keywords = _keywords() + keywords.pop("progress") + simulation = Simulation.from_capillary(RADIUS, LENGTH, "He", 2.0, **keywords) + assert simulation.settings.quadrature is not None + assert not simulation.translate().flags.mode_averaged + + +@pytest.mark.medium +@pytest.mark.parametrize( + ("radius", "gas", "extra"), + [ + (Linear(RADIUS, 0.8 * RADIUS, LENGTH), "He", {}), + (RADIUS, "He", {"envelope": True}), + ( + 15e-6, + "Ar", + { + "lattice": TubeLattice(0.7e-6, 5e-6, 6), + "energy": 1e-6, + "wavelength_limits": (300e-9, 3e-6), + }, + ), + ], + ids=["taper", "envelope", "antiresonant"], +) +def test_the_switch_runs_the_capillary_variants(radius, gas, extra): + """A tapered bore, an envelope grid and a tube lattice run mode-averaged.""" + output = prop_capillary( + radius, LENGTH, gas, 2.0, **_keywords(**extra), mode_average=True + ) + assert output.metadata is not None + assert output.metadata.simulation_kind == "mode_averaged" + + +@pytest.mark.medium +def test_the_switch_runs_a_supplied_gas_mode(): + """`prop_waveguide` takes the switch for its supplied mode.""" + output = prop_waveguide( + [MarcatiliMode(RADIUS)], LENGTH, "He", 2.0, **_keywords(), mode_average=True + ) + assert output.metadata is not None + assert output.metadata.simulation_kind == "mode_averaged" + + +@pytest.mark.parametrize( + ("given", "match"), + [ + ({"modes": 2}, "needs exactly one mode"), + ({"n_r": 8}, "n_r given beside mode_average=True"), + ({"n_theta": 4}, "n_theta given beside mode_average=True"), + ({"radial_rule": "gauss"}, "radial_rule given beside mode_average=True"), + ], +) +def test_the_switch_refuses_what_needs_a_rule(given, match): + """More than one mode, or a keyword that shapes the rule, is refused by name.""" + keywords = _keywords() + keywords.pop("progress") + with pytest.raises(DriverError, match=match): + Simulation.from_capillary( + RADIUS, LENGTH, "He", 2.0, **keywords, **given, mode_average=True + )