Recipes and the command line#
A recipe is a simulation written down as a TOML file: the keywords of one route’s
call, with each quantity in its unit, and the settings the run executes with. solprop
reads it, checks it, builds and runs it, writes it back, and exports it as the plain
Python script it stands for. The same file runs from a script, from the solprop
command, from a batch job or from a front end, and a recipe and the call it stands for
build the same simulation, configuration hash included.
Recipes describe the keyword routes gnlse, capillary and waveguide, and the
free-space routes radial, free and planar, whose keywords are objects
(below). They are specified in
SEP 41.
A recipe#
[recipe]
format = 1
route = "capillary"
title = "RDW in a helium capillary"
[build]
radius = "125 um"
length = "1 m"
gas = "He"
pressure = ["1.2 bar", "0 bar"] # entrance to exit: a gradient to vacuum
wavelength = "800 nm"
wavelength_limits = ["100 nm", "1500 nm"]
trange = "200 fs"
fwhm = "7.5 fs"
energy = "275 uJ"
modes = 2
n_save = 51
seed = 20261027
[execution]
fields = "planes"
field_planes = [0, -1]
This is the first lesson’s run (resonant dispersive waves),
shipped as Recipe.example("rdw"). A recipe has five tables and nothing else at the top
level, so a keyword added to solprop later can never collide with the format:
Table |
Holds |
|---|---|
|
|
|
the keywords of the route’s |
|
|
|
values solved from a design target (a later release) |
|
a scan over parameter paths (a later release) |
The names in [build] are the keywords of
from_capillary(),
from_gnlse(),
from_waveguide(),
from_radial(),
from_free() and
from_planar(), with their meanings and
defaults. A keyword left out takes solprop’s default when the recipe is built, so a recipe
follows solprop’s improvements; the output records what ran. Keys that choose files,
terminals or resume behaviour (output, progress, progress_period, resume,
snapshots, tdse_archive) are refused in a recipe: the caller or the command line
supplies them. An unknown key is refused with the nearest valid one.
Values#
Quantities. A number is read in the keyword’s own unit, which is SI except for pressures, which are in bar (conventions):
pressure = 1.2is 1.2 bar. A string is a decimal number and a unit,"125 um","7.5 fs","2e14 W/cm^2", converted exactly in decimal and rounded once, so"800 nm"is the same double as the literal800e-9. The unit’s dimension must be the keyword’s:"125 um"for a pressure is refused. Writeumfor micro (µmis read too); a temperature is in K only. Elements ofphasescarry their own units (rad, s, s², …), and so do those ofbetas(sⁿ/m by order).None. Leaving a key out gives its default; where that default is not
Nonebut the keyword takesNone(noise,boundary_check), the string"none"saysNone.Complex numbers are
{re = 1.45, im = 0.0}; an integer beyond 64 bits, such as a drawn noise seed, is a decimal string.Arrays are lists of at most 10 000 numbers. A longer or a two-dimensional array is a data file,
{file = "betas.npy", sha256 = "…"}, read without pickling and checked against its hash. The path is relative to the recipe, and must stay inside its directory unless the caller allows otherwise.Objects are tables with a
typekey naming a registered type, and that type’s keywords:pulses = [{type = "SechPulse", wavelength = "835 nm", width = "100 fs", peak_power = "9.8 W"}]. The registered types are the pulses (GaussianPulse,SechPulse,load_spectrumfor a measured spectrum), the pressure profiles (PressureGradient.two_point,.from_stages,Constant,Linear,PiecewiseLinear), the mixtures (Mixture.fill,.partial_pressures,.air), the grids (EnvelopeGrid.from_limits,FieldGrid.from_limits),StatsSpec,BoundaryCheck,FixedSchedule.uniform, the integrators (Multistep,RungeKutta,LowStorageRungeKutta),absorber(built on the route’s grid),TubeLattice,CoherentWall,MaterialSelection, the supplied modes (MarcatiliMode,RectMode,SlabMode,ModeCollection.slab), and the free-space types: the grids (RadialGrid,FreeGrid,LineGrid,PlanarGrid),SpatialMedium, the responses (KerrResponse, withgamma3for optmat’s coefficient of a gas,EnvelopePlasmaResponse,PlasmaResponse,OptmatRate), the beams (SpatialPulse,PlanarPulse),SimulationSettings,PIController,default_specandSpatialAbsorber. A recipe never imports a module or evaluates code.
Supplied modes. On the waveguide route, and for the capillary’s modes, a mode is
a table of its geometry, cladding and loss. Its fill is the route’s gas, pressure and
temperature, which every supplied mode is rebound to, so a mode table names none: the
planar guide of hollow planar waveguides, per metre of y, is
[recipe]
format = 1
route = "waveguide"
[build]
modes = { type = "ModeCollection.slab", half_gap = "63.5 um", orders = [1, 3, 5] }
length = "10 cm"
gas = "Ar"
pressure = "1 bar"
wavelength = "800 nm"
wavelength_limits = ["650 nm", "1000 nm"]
trange = "1.2 ps"
fwhm = "100 fs"
energy = 0.5 # J per metre of y, on a slab set
envelope = true
plasma = false
raman = false
and a list of tables gives the modes one by one, modes = [{type = "RectMode", half_width_x = "50 um", half_width_y = "10 um", polarisation = "y"}]. A guide that
solves its own fields (provider, the general modal route)
has no table form: its knots and node rule are taken from the run’s grid and the
provider, so it is written in Python.
Parameter paths name a value inside the recipe: the keyword, then .name for a
keyword of an object, [i] for an element of a list and ["key"] for a key of a mapping,
as in pressure, pulses[1].energy or betas[2]. Recipe.get and Recipe.set take
them, and so does every error a recipe or a route raises about a value, in its
parameter attribute.
Free-space routes#
On radial, free and planar the constructor takes objects, and a recipe writes each
as a table under the keyword’s name: grid (the time grid), space (the transverse
grid), medium (not on planar, whose slab channels carry the fill), responses,
initial (the beam) and settings. The
Cartesian tutorial is
Recipe.example("cartesian_beam"):
[recipe]
format = 1
route = "free"
[build]
grid = { type = "EnvelopeGrid.from_limits", length = "20 mm", reference_wavelength = "800 nm", wavelength_limits = ["400 nm", "2 um"], trange = "200 fs", thg = false }
space = { type = "FreeGrid", radius_x = "6 mm", n_x = 32, radius_y = "7.5 mm", n_y = 40 }
medium = { type = "SpatialMedium", material = "Ar", pressure = "4 bar" }
responses = [
{ type = "KerrResponse", chi3 = { type = "gamma3", gas = "Ar", wavelength = "800 nm" }, thg = false },
]
noise = "none"
[build.initial]
type = "SpatialPulse"
temporal = { type = "GaussianPulse", wavelength = "800 nm", fwhm = "30 fs", energy = "1.5 nJ" }
waist = "2 mm"
[build.settings]
type = "SimulationSettings"
controller = { type = "PIController", rtol = 1e-6, atol = 1e-13 }
save_planes = ["20 mm"]
dz_init = "2 mm"
dz_max = "2 mm"
stats = { type = "StatsSpec", names = ["energy"] }
A recipe holds the from_* form, so the temporal absorber and its scheme are keywords
of the settings table, absorber = { type = "absorber" } built on the grid, and not
of [build], although prop_radial, prop_free and prop_planar take them as
keywords too. The settings table takes the stepping, the planes, the statistics, the
absorber, the backend and the precision; fields, field_planes and resume_history stay
in [execution], and the fields a route’s builder sets (the density, the
transverse quadrature) are not the recipe’s. On the planar route a slab mode names its
fill, { type = "SlabMode", half_gap = "63.5 um", gas = "Ar", pressure = "1 bar" },
because the fill of its channels is the run’s; on the waveguide route the same
keywords are refused, since every supplied mode is rebound to the route’s fill. A
launch built as an array, a Frame and a design pin have no table form on these routes.
From Python#
from solprop.recipe import Recipe
recipe = Recipe.example("rdw")
recipe.set("energy", "250 uJ") # checked against the keyword's unit and range
print(recipe.check()) # problems, each at its parameter; [] here
script = recipe.to_python("rdw.h5") # the plain script this recipe stands for
simulation = recipe.build() # a Simulation, not yet run
Recipe.load reads a file and Recipe.loads text; recipe.save writes it. A recipe that
has not been edited saves its text verbatim, comments included; an edited one is written
canonically, each quantity in its keyword’s display unit, without comments.
Recipe.from_keywords("capillary", radius=125e-6, ...) writes a call made of plain
values as a recipe, and refuses an object or a function, which have no table form: a
recipe is built into objects, never made from them. recipe.run(output) builds and runs.
recipe.check() decodes every value against the keyword’s metadata, then builds the
simulation and translates it, stopping before a backend is built: a refusal that depends
on several values (a grid that does not cover the pulse, two conflicting keywords) is
reported there, with the keywords concerned, and so is every warning solprop gives. It
costs a translation, a fraction of a second on the capillary example.
What a recipe cannot hold: a callable (a beta function, a loss curve, a custom
absorber’s rate), a type that is not registered (a mode or a tabulated index as the
GNLSE fibre, CustomPulse, a TDSE response, a material object as a mode’s cladding or
a medium, a guide built from a provider, an ApertureSet, a Raman or dispersive Kerr
response on a free-space route), and an array: one given to Simulation.with_input, or
a free-space launch built as a spectrum. to_python() is the way out: export the recipe
and edit the script.
A grid to start from. solprop.grid.suggest() proposes wavelength_limits
and trange from the input pulses: a band ten input bandwidths wide each side of the
carrier, and at least half the carrier frequency on the gnlse route or all of it on the
capillary route, and a window thirty input durations long beyond the pulses’ delays. It
knows nothing of the medium or the length, so it is a starting point: check the spectrum
at the band’s edges and the boundary monitor’s verdict.
Pins#
A [pins] table sets a value from a design target instead of a literal. The pressure
that puts the zero-dispersion wavelength at 500 nm, and the energy that makes the pulse a
fourth-order soliton there:
[pins]
pressure = {target = "zdw", wavelength = "500 nm", search = ["0.01 bar", "20 bar"]}
energy = {target = "soliton_order", order = 4}
Target |
Keys, beside |
Pins |
|---|---|---|
|
|
the guide or fill: |
|
|
the same; the pump is the recipe’s |
|
|
a pulse’s |
A pin on the guide or fill needs search, the range its solver
(solprop.tools.solve_parameter()) samples, in the keyword’s unit; it is solved
on the launch mode (the first) at the entrance. A soliton-order pin is closed form, on
that mode with the fill’s Kerr coefficient, after the guide’s pin. So at most one
parameter of the guide or fill is pinned, then the pulses; a pinned value is not also
written in [build], and a pressure gradient, or one element of an array, cannot be
pinned. root chooses among several roots: "lowest" (the default), "highest" or an
index.
recipe.resolve() returns the recipe with each pin replaced by its value, and
recipe.pin_roots("pressure") the solver’s DesignRoots: every
root, or none with the range the target does reach, for a front end to offer. A target
not met in its range is an issue of check() on the pinned path. A build solves the pins
(each scan point solves its own), a run records both the authored recipe and the
resolved one, and to_python() writes the solver calls
(solve_pin(),
solve_pulse()) before the route’s. Solving a guide’s pin
costs a few hundred evaluations of one mode, about a second for a capillary. A design that
sets two values at once, such as capillary_design(), is not a pin:
call it and write its values in [build].
Previews#
Before a run, recipe.modes() returns the mode set a guided route builds (the
capillary and waveguide routes, the slab channels of the planar route and the
gnlse route’s one specified mode, without a fill), at the
launch position or at z: each mode’s label, its effective index (neff_minus_one),
beta, beta1, beta2, beta3 and loss over the grid’s band, its effective area (a
slab mode’s effective width), its nonlinear coefficient gamma where its fill has Kerr
data (with the fill’s n2 at the grid’s reference wavelength), and its
zero-dispersion wavelength in the band. It also gives each mode’s phase matching to a
soliton at the pump wavelength in the launch mode, the set’s first: the
phase_mismatch of a dispersive wave in that mode over the band, and the phase-matched
wavelengths rdw between the band’s short edge and 0.9 of the pump (intermodal for every
mode but the first). With an unchirped sech or Gaussian pulse above the soliton
threshold, in anomalous dispersion, and a nonlinear coefficient (the recipe’s gamma, or
the fill’s Kerr data at the preview’s position), the soliton’s nonlinear wavenumber is
its largest constituent’s, and the set says so (nonlinear, with pump); otherwise it
is the linear condition. Its cost is the mode
solve. recipe.preview_input()
returns the input pulse on the run’s grid as the launch sets it, without noise, so a
preview draws nothing: its power in time, its spectrum, its energy and its peak power,
read through the processing functions that read a run’s file. On a free-space route it
gives the beam: the power and energy through the launch plane in the medium, and the
fluence there on the transverse grid with its axes (fluence, and axes as (r,),
(x,) or (y, x)), measured as a saved plane is
(spatial_fluence()). Both solve the pins first.
Scans#
A [scan] table varies parameter paths over a grid of points. name is required: it
names the scan’s files and derives each point’s seed; base_seed may set the root of
those seeds. Every other key is a path with a list of values, or a range
{start, stop, num, spacing = "linear" | "log"}, read in the unit of the keyword the
path ends on:
[scan]
name = "rdw-pressure"
pressure = {start = "0.6 bar", stop = "1.4 bar", num = 9}
energy = ["200 uJ", "275 uJ"]
recipe.scan() returns the Scan, a builder that sets each
point’s values, passes the point’s seed and builds (a picklable one, for the Pool
executor), and the keywords run_scan takes from [execution]:
scan = recipe.scan()
run_scan(scan.scan, scan.build, output="scan.h5", **scan.keywords)
run_scan takes fields and field_planes. The table’s other setting, resume_history,
goes to each point’s run.
The recipe in the output#
A simulation built by recipe.build() carries its recipe, and its first run records it
in the output beside the run, as the recipe was written and as it resolved: the recipe
that rebuilds the run, seed included (the file’s layout).
A change the recipe would not describe drops it with a warning, and the run then records
none: with_input, with_mixture, or a new value of a setting other than fields,
field_planes and resume_history.
The parameter metadata#
solprop.recipe.metadata.parameters() returns, for each keyword of a route, the forms its
value may take with their units and ranges, the unit a front end shows it in, its group
(geometry, medium, nonlinear, input, grid, numerics, output), whether a
newcomer sees it, a one-line summary and the manual section that documents it. Where a
default of None is resolved by a rule, resolver(values) says what it will be and why:
from solprop.recipe import parameters
rtol = parameters("capillary")["rtol"]
print(rtol.resolver({"precision": "single"})) # the capillary's tolerance in binary32
The same call takes a registered type’s name for the keywords of its table, so a second
pulse, a pressure gradient, a mode or a free-space grid is described as a route keyword is:
parameters("GaussianPulse")["fwhm"] has the unit s, the display unit fs and the
summary a front end’s field shows. A type’s table leaves out what the type receives from
the route (the absorber’s time grid) and the keywords a recipe may not give.
A test holds the tables to the constructors’ signatures and to the registry’s units, so they cannot drift from them.
What a route and a species admit#
solprop.capabilities.route() gives a route’s keywords, the allowed values of each
enumerated one, and the combination rules its builder enforces: raman needs kerr,
a TDSE response needs a helium fill, save_planes and n_save are not given together.
Each rule has a stable id and a reason, and a refusal that enforces one carries the id as
its rule beside the keywords as its parameter, so a front end can mark the fields in
conflict and say why. A refusal that depends on a value, such as a grid that does not
cover the input or a pressure outside a model’s range, is not a rule: check() finds it.
solprop.capabilities.species() gives what a built-in gas brings: optmat’s
ionisation-rate models with their status, whether it has a Raman response and its
status, and the wavelength and temperature ranges its models hold over.
from solprop import capabilities
capillary = capabilities.route("capillary")
print(capillary.choices["shape"]) # ('gauss', 'sech')
for rule in capillary.rules_for("raman"):
print(rule.id, rule.keywords, rule.reason)
nitrous_oxide = capabilities.species("N2O")
print(nitrous_oxide.raman, nitrous_oxide.raman_by_default) # provisional False
A test reads the source for every refusal marked with a rule and holds it to
solprop.capabilities.RULES.
The command line#
solprop run rdw.toml -o rdw.h5 [--progress log] [--resume]
solprop check rdw.toml # print the issues; exit 1 on an error
solprop export rdw.toml --python # print the equivalent script
solprop resume rdw.h5 # continue a run from its file alone
solprop scan ... # python -m solprop.scan, unchanged
python -m solprop is the same command. check exits 0 when the recipe builds (warnings
included), 1 when it does not, and 2 when the file cannot be read. run and resume stop
cleanly on Ctrl-C, writing the state, and solprop resume continues it
(resuming from the file alone).
The example recipes#
Recipe.examples() lists the recipes solprop ships, each the run of a gallery example,
and tested to build that example’s configuration hash. A front end starts a new setup
from one of them. The gnlse, capillary, free and planar routes have examples
(cartesian_beam, planar_hybrid and planar_compressor on the free-space routes).
Each gallery script on a route a recipe describes that has no recipe is listed, with
its reason, in the test that holds the examples to the gallery, so the list stays true:
the
waveguidescripts, which build guides from providers, launch throughSimulation.with_inputor clad a guide in a metal, and the tabulated-mode and step-index tutorials, whose modes are not registered;the radial tutorial, the filament and crossing-beam lessons, and the line, low-storage, four-wave-mixing and far-field tutorials, which launch a beam built as an array;
the transverse-window and capillary-coupling tutorials, whose beam energy is computed from the material functions, which a literal in a recipe would freeze;
the two dispersive-Kerr scripts, whose pressure is a numerical phase-matching root, and the pressure-and-energy scan, built with
with_mixtureandwith_input.
So the radial route has no example yet; its calls are tested against the free-space
guide’s.
This page documents the functionality proposed and implemented by SEP 41.