Python¶
import ultrafast as uf
from ultrafast import build as b
Installing¶
pip install -e "python[hosted,plot,test]" # from the repository
python python/tools/build_engine.py # hosted engine -> python/src/ultrafast/bin/hosted (needs .NET 10)
python python/tools/build_engine.py --aot # native library -> python/src/ultrafast/bin/native (no .NET at run time)
The package looks for the engine in $ULTRAFAST_LIBRARY, then ultrafast/bin/native, ultrafast/bin/hosted,
then a repository build. uf._native.library().mode says which back-end is in use.
API¶
uf.System.load(path | dict | json) |
load a system; .to_dict(), .validate() |
uf.Run(system, spec) |
spec = path, dict or JSON text |
run.time_response(name, fn, carrier_resolved=False) |
fn(ctx) writes ctx.dn_re / ctx.dn_im or returns dn_re or (dn_re, dn_im) |
run.index(name, fn) |
fn(omega) -> n |
run.filter(name, fn) |
fn(omega) -> T or (T, phase) |
run.beam(name, fn) |
fn(z) -> w or (wx, wy) |
run.array_function(name, fn) |
fn(x) -> y (e.g. a Raman h(t)) |
run.element(name, fn) |
user element: fn(ctx: ElementContext) returns the new spectrum ax or (ax, ay) (Active elements) |
run.execute(progress=None) |
progress(element_id, i, n, fraction); return True to cancel |
uf.run(system, spec, callbacks={name: fn}, elements={name: fn}) |
one call; with "fit" in the spec it runs the calibration and returns the best run (result.summary["fit"], Fitting plugin) |
uf.crystal.phase_match(crystal, process, type=None, fundamental=, pump=, signal=, idler=, noncollinear=, qpm=, length=, temperature_c=, ...) |
phase-matching calculator (Crystals) |
uf.crystal.crystals() |
built-in crystal names |
uf.calc.tbp(shape), uf.calc.limit(shape, duration= / bandwidth_hz= / bandwidth_m=, center=), uf.calc.analyze(duration=, bandwidth_hz= / bandwidth_m=, center=), uf.calc.measure(time_s, intensity, spectrum, wavelength_m=) |
pulse calculator: time-bandwidth product, transform limit, stretch factor, GDD (Pulse calculator) |
b.system(name, elements) |
a list (or b.Sequential([...])) runs in order; b.Branched([...]) says where each element's light goes (Branched systems) |
b.link(element, next, inputs=, loop=) |
an element of a Branched system: next is "bs:1", {"id": "bs", "port": 1}, "end", "virtual", or a dict of those by output port |
b.virtual_input(pulse=, delay_s=, phase_rad=, coherent_with=), b.loop(kind, max_round_trips=, tolerance=) |
a beam on an input port with no beamline; the settings of a loop |
b.beam_splitter, b.polarizing_splitter, b.leak_mirror, b.dichroic, b.delay |
the multi-port elements; the same builders work in a Sequential system with output= |
b.aux_beam(role, from_port=True, radius_m=) |
a crystal input fed by a link in a Branched system |
result.summary |
the summary.json dict; a Branched run adds summary["graph"] |
result["output_spectrum"], result.arrays, result.names() |
NumPy arrays (copied, cached) |
result.instrument("spectrometer") |
values + arrays of one instrument |
result.provenance, result.write(dir) |
|
uf.load("results") |
open a CLI results folder (same interface as a Result; no engine needed) |
uf.plot(result, "overview" | "temporal" | "spectrum" | "phase" | "losses" | "elements") |
quick matplotlib plots |
ultrafast.plotting.report(result_or_folder, out_dir) |
all figures as PNG/SVG + report.pdf (see Plotting) |
uf.study(system, spec, parameters=[...], outputs=[...], mode="scan" \| "optimize" \| "sensitivity", **options) |
parameter studies (Parameter studies) |
uf.retrieve("pcgpa" \| "epie" \| "dscan", trace, instrument=, fundamental=, material=, output=) |
pulse retrieval; returns a ready-to-use "pulse" (Pulse retrieval) |
uf.materials.available(), .show(name), .import_page(path, name=), .refractiveindex(page, pressure_bar=) |
material library and refractiveindex.info pages (Materials) |
uf.devices(selftest=False, benchmark=False, backend=None, device=None) |
compute back-ends and devices (CPU, CUDA, OpenCL), optional self-test against the CPU (GPU) |
uf.benchmark(backend=None, device=None) |
seconds per run on the CPU and each device, with the speed-up (Speed) |
uf.set_default_backend(backend=None, fft=None) |
process defaults: back-end of runs without compute.backend ("auto" uses a GPU when it helps), CPU FFT provider |
b.compute(backend, threads=, fft=, device=, gpu_min_points=) |
the run spec's "compute" section: b.run_spec(..., compute=b.compute("auto")) |
uf.command({"command": ...}) |
any engine command as JSON (the same as the C function uf_command) |
ultrafast.types |
TypedDicts of every element and section (PlateElement, RunSpec, ...): editor completion and type checking |
uf.version(), uf.capabilities() |
|
uf.plugins.installed(), uf.plugins.module(id), uf.plugins.search_paths() |
installed plugins and their Python modules (Plugin system) |
ultrafast.plotting.register_figure(name, maker, after=None) |
add a figure to reports and ultrafast-plot (plugins) |
ResponseContext fields: t, t_lab, intensity, intensity_x, intensity_y, instant_intensity,
field, t_arrival, z, length, omega0, area, carrier_resolved, element_id, dn_re, dn_im,
x, y, r, theta, spatial (position of the grid point on spatial models; 0 / False in 1-D)
(NumPy views valid during the call).
Position-dependent properties (indexProfile, absorptionProfile, n2Scale, gain dopingProfile) can be Python
functions: uf.run(..., fields={"name": lambda x, y, z: ...}) or Run.field("name", fn), referenced as
"@name" (Position-dependent properties). Exceptions raised in callbacks abort the run and are re-raised by
execute().
ElementContext fields: omega (absolute, ascending), t, omega0, dt, arrival, element_id, params
(the element's params dict), ax, ay (complex spectra, copies), set(ax, ay), time_x().
Package structure¶
Find things by what they are. The lowercase module names follow PEP 8; classes and factories are CapWords. Everything that was public in 2.2.0
still imports (a test checks a list of the old names); only ultrafast.diagram moved, and its old name warns.
| module | contents |
|---|---|
ultrafast |
run, load, System, Run, Result, study, retrieve, ... and the submodules below |
ultrafast.build |
the dict builders with TypedDict autocompletion (b.beam_splitter(...)); the low-level truth, unchanged |
ultrafast.system |
Sequential, Branched, link, loop, virtual_input, system, System, load, save |
ultrafast.components |
the same builders by kind with CapWords names: optics (Mirror, Lens, Grating, Mask, Wavefront ...), splitters (BeamSplitter, Dichroic, Delay ...), polarisation, crystals, fibres, amplifiers (Gain, Pump, ...), media (Bulk, Gap) |
ultrafast.sources |
Pulse, Cw, GaussianBeam, MeasuredSpectrum, Pedestal, InitialProfile ... |
ultrafast.instruments |
one factory per kind: Spectrometer, Frog, DScan, Camera, WavefrontSensor ..., plus Axis, Detector |
ultrafast.algorithms |
NumPy-only algorithms: zernike (Noll polynomials, fit, evaluate), retrieval, optimisation |
ultrafast.results |
Result, SavedResult, load, curve metrics (fwhm, rms_width, strehl_marechal) |
ultrafast.plotting |
figures and reports (below) |
The CapWords names are the same functions as the snake_case builders (components.BeamSplitter is build.beam_splitter), so the typed return
values and editor completion are unchanged. Builders that describe a whole run or a global setting stay in build only (run_spec, physics,
grid, compute, transverse, material, gas, override, ...). import ultrafast does not import matplotlib or Pillow.
from ultrafast.system import Sequential, Branched, link
from ultrafast.components import BeamSplitter, Waveplate, Mirror
from ultrafast.sources import Pulse
from ultrafast.instruments import Spectrometer
Building systems in Python¶
Gain media: b.pump(...), b.gain(...), b.multipass_amplifier(...), b.regenerative_amplifier(...)
(Gain media). Lasers: ultrafast.laser.Oscillator (fluent builder), b.saturable_absorber(...),
b.soft_aperture(...) (Oscillators).
ultrafast.build returns plain dicts (save them with b.save(d, "x.ufs")):
sysd = b.system("mpc", [
b.interface("win_in", "Air", "FusedSilica", coating="ar"),
*[b.bulk(f"pass{k}", "Ar2bar", 0.55, beam=b.gaussian_beam(150e-6, 0.275, 1.1)) for k in range(20)],
], materials={"Ar2bar": b.material(gas=b.gas("Ar", 2.0))}, coatings={"ar": {"kind": "ar", "R": 0.002}})
spec = b.run_spec(b.pulse(1.03e-6, 220e-15, energy=200e-6),
instruments=[b.instrument("spectrometer"), b.instrument("compressor")],
overrides=[b.override("element:pass*", "timeResponse", "@mymodel", mode="add")])
Helpers: system, bulk, gap, interface, mirror, dispersion, grating, grating_pair, mask, chirped_mirror, aperture, waveplate,
polarizer, filter_element, probe, gaussian_beam, constant_beam, sampled_beam, material, sellmeier, gas,
coating_gdd, coating_quarter_wave, pulse, instrument, override, run_spec, save, and for active elements:
crystal (χ(2) element: process, type, cut, noncollinear, qpm, aux, output, main_role, record,
overlap, chi3, deff_pm_per_v, temperature_c, transverse, ...), aux_beam(role, pulse | from_results=,
radius_m=, delay_s="auto"), transverse(model, points=, radius_m=, absorbing_boundary=, spectral_samples=, ...),
initial_profile(kind, geometry=, radius_m=, ...), thermal_lens(...), physics(beam_tracking=, spatio_temporal=), lens(id, focal_length_m, material=),
user(id, callback, params=); components and fibres: fiber, plate, nd_filter, thin_film_polarizer, faraday_rotator,
wavefront, pedestal, waveplate(..., material=, order=, construction=) (Optical components).
Types. Every builder returns a TypedDict from ultrafast.types (b.plate(...) is a PlateElement), and
dictionaries you write by hand can be annotated the same way, so editors complete keys and type checkers (mypy,
pyright) flag misspelt ones:
from ultrafast.types import PlateElement, RunSpec
plate: PlateElement = {"id": "w", "type": "plate", "material": "FusedSilica", "thicknessM": 5e-3}
plate_typo: PlateElement = {"id": "w", "type": "plate", "thicknesM": 5e-3} # mypy: Extra key "thicknesM"
The full list of functions with signatures: Python API (generated).
Sampling and measured data (core; Measured data):
sampling(plane, aperture, radius_m=, width_m=, focal_length_m=, center_x_m=, na=),
measured(file, values=, sigma=, normalize=, background=, columns=, range_x=, resolution_m=, resolution_s=, match_resolution=, scale=, ...)
for an instrument's recording, and measured_spectrum(...) / measured_spectrum_csv(...) (with background=,
threshold=, resolution_m=, deconvolve=, extrapolate=) for a measured input pulse. The comparisons come back
typed: res.comparisons (a list of ultrafast.measured.Comparison), res.comparison(name), and
uf.measured.curve(res, name) / trace(res, name) for the compared arrays.
spec = b.run_spec(b.measured_spectrum_csv("data/input.csv", 200e-9, background="auto"),
instruments=[b.instrument("spectrometer", name="spec", resolutionM=0.1e-9,
sampling=b.sampling("far", "fibre", radius_m=25e-6, focal_length_m=0.1),
measured=b.measured("data/spectrum.csv", background="auto", sigma=0.005,
resolution_m=0.5e-9))])
res = uf.run(system, spec)
c = res.comparison("spec") # chi2, reducedChi2, rmsError, notes (points outside the range, resolution)
curve = uf.measured.curve(res, "spec") # measured_x, measured_y, simulated_on_measured (NaN outside the range)
Calibration fits against these comparisons (fit=, fit_parameter(...), transforms) come from the fitting plugin's
module ultrafast_fitting (Fitting plugin).
pump = b.aux_beam("pump", b.pulse(515e-9, 0.8e-12, energy=200e-6), radius_m=0.6e-3)
sysd = b.system("nopa", [b.crystal("nopa", "BBO", 2e-3, process="opa", noncollinear="magic", aux=[pump],
output="signal", beam=b.gaussian_beam(0.55e-3))])
res = uf.run(sysd, b.run_spec(b.pulse(800e-9, 15e-15, energy=50e-9)))
res.elements()[0]["values"]["conversionEfficiency"]
Examples: docs/examples/GettingStarted/quickstart/quickstart.py, docs/examples/GettingStarted/custom-index-python, docs/examples/Nonlinear/mpc-argon/make_mpc.py,
docs/examples/GettingStarted/user-element/dazzler.py, docs/examples/Nonlinear/ppln-dfg/compare_pynlo.py.
Performance notes¶
Callbacks are called once per solver stage with whole arrays: keep them vectorised (NumPy), avoid Python loops
over samples. Elements with a callback response run on the CPU even when a GPU back-end is selected; elements
without one can run on the GPU (b.compute("auto"), GPU). The ctypes round trip costs microseconds; a typical nonlinear element calls a time response a few
thousand times.
Plotting results¶
ultrafast-plot (installed with the package; or python -m ultrafast.plot, or simply
python python/src/ultrafast/plot.py from a checkout without installing) turns a results folder written by the
CLI into figures. It needs only NumPy and matplotlib, not the engine:
ultrafast run docs/examples/Nonlinear/mpc-argon/mpc.ufs docs/examples/Nonlinear/mpc-argon/run.json -o results
ultrafast-plot results/ # -> results/plots/{system,overview,instruments,measurements,fit,probes,elements,byproducts,...}.png + report.pdf
ultrafast-plot results/ --show --db # open windows too; spectra in dB
ultrafast-plot results/ --only overview --format svg,pdf --dark
ultrafast-plot runA/ runB/ --labels "no CM" "with CM" --out compare/ # each run + compare.png
| figure | contents |
|---|---|
system |
schematic of the system: every element (repeated units such as multipass-cell passes collapsed to "× N"), the beam line coloured by its carrier wavelength, aux beams coming in, by-products leaving, probes, and numbered instrument locations (○ instrument, □ virtual); a numbered list of all instruments and where they sit |
overview |
the system schematic, input/output pulse (+ ideally compressed pulse), spectrum, spectral phase with offset and group delay removed (where the spectrum is above 1 %), energy / FWHM / cumulative B through the system, loss budget by mechanism, key numbers and warnings |
instruments |
one panel per instrument, in system order, titled with its number and location: cameras and wavefront maps (images), Wigner / spectrogram, field probe, space-time maps, spectrometer (dB), autocorrelator, compressor / scope, d-scan trace and FWHM vs GDD, contrast, polarimeter (Stokes vs wavelength); unknown or user instruments are drawn generically (2-D arrays as images against their matching 1-D axes) |
measurements |
runs with measured data (core): per instrument the measured points, the simulated reading (with its ±1σ band after a fit) and the residual; measured / simulated / difference images for FROG, d-scan and camera traces; scalar readings in units of σ |
fit |
calibration fits (fitting plugin): fitted parameters ± σ (and start values), χ² per iteration, the parameter correlation matrix, χ² per instrument before and after |
probes |
spectrum (dB) and power heat maps, one row per probe, input at the bottom |
elements |
small multiples per element: energy, FWHM, peak power, cumulative B, system GDD, peak intensity, LIDT margin, pulse GDD |
oscillator |
oscillators: build-up over the round trips, evolution of the pulse and the spectrum (heat maps), the final pulse with soliton / QML diagnostics |
gain |
gain and amplifier elements: small-signal gain spectrum with the output spectrum, inversion along z (or across the beam), build-up per pass / round trip, pulse-train convergence |
compare |
output pulse and spectrum of several runs overlaid (with more than one folder) |
From Python, every panel is also a function taking a live Result or a folder:
from ultrafast import plotting as up
res = up.load("results")
fig = up.overview(res); fig.savefig("overview.png", dpi=200)
up.spectrum(res, ax=my_axes, db=True) # single panels: temporal, spectrum, spectral_phase, evolution, losses, key_numbers
up.report(res, "results/plots", formats=("png", "svg"))
up.compare([up.load("runA"), up.load("runB")], labels=["A", "B"])
up.measurements(res) # measured vs simulated (runs with "measured" data)
up.fit(res) # calibration report (runs with "fit")
up.set_theme(dark=True)
Package, themes, Zernike plot and animation¶
ultrafast.plotting is a package (style, data, time_domain, spectral, run_figures, spatial, instrument_panels, diagrams, zernike,
animate); every old function name (plotting.overview, plotting.report, plotting.register_figure, ...) is still importable from
ultrafast.plotting, and the fitting plugin's fit figure uses the same helpers.
from ultrafast.plotting import zernike as zp, animate
fig = zp.decomposition(res.instrument("wfs")) # wavefront, Zernike fit, residual, Noll bar chart, rms and Strehl
fig = zp.decomposition([before, after], ["before", "after"], radius=1e-3) # side by side on one colour scale
fig = zp.decomposition(field, x=x_m, y=y_m, wavelength=800e-9) # a complex field: its phase is the wavefront
zp.animate_decomposition(list_of_wavefronts, "correction.gif") # one frame per wavefront
animate.animate(frame_fn, frames, "converge.gif", fps=15) # any figure per frame -> animated GIF (Pillow; MP4 too if ffmpeg exists)
animate.check_budget("docs/examples") # PNG over 300 kB or GIF over 2.5 MB
The Zernike convention is the engine's (Noll index from 1, rms-normalised, m < 0 the sine term), so coefficients agree with the
wavefrontSensor instrument (a test compares them to 1e-6). The decomposition accepts a wavefrontSensor result, a map with its axes, or a complex field.