Python Interface Guide
This guide is the recommended starting point for new HASEonGPU simulations in Python. It explains the workflow and concepts: mesh construction, material properties, pump propagation, ASE execution, and time stepping with the high-level Python objects.
For generated signatures, class members, and direct object lookup, use the
Python API Reference. The older low-level
calcPhiASE(...) interface is still supported and is documented separately in
Legacy Python Interface.
The current interface allows users to describe the physical problem using
separate high-level abstraction objects. These objects define the
MeshTopology and the physical properties of the GainMedium.
Absorption and emission cross sections are described through a
SpectralDecomposition.
The pump setup (PumpProperties) and the settings for the C++ PhiASE
computation can be configured independently.
Together, these configuration objects can be used to run a time-stepped
physical Simulation of the laser crystal. During the simulation, the
evolution of gain propagation and population inversion can be monitored.
Installation
Install the HASEonGPU Python package from the repository root. The recommended path for performance-sensitive use is a source build with native host optimizations enabled:
CMAKE_ARGS="-DHASE_NATIVE_OPTIMIZATIONS=ON" python3 -m pip install .
This builds and installs the C++ extension locally and installs the Python
dependencies declared in pyproject.toml. HASE_NATIVE_OPTIMIZATIONS=ON
enables host-specific -march=native and -mtune=native tuning for the
build machine. Disable it when building redistributable wheels or binaries for
unknown CPUs.
Compiler Runtime
The Python extension is built with an $ORIGIN RPATH so libraries installed
next to the wheel extension are preferred at import time. If you change the GCC
or C++ runtime version used to build HASEonGPU, rebuild and reinstall the wheel:
python3 -m pip install --force-reinstall --no-cache-dir .
Concept Pages
Python interface guide
Minimal Example Tutorial
The Python interface guide is built around the objects that appear in a physical ASE
simulation. First describe the crystal geometry. Then attach material and
state data to that geometry. Then describe the spectra and pump. Finally,
configure the ASE solver and let Simulation advance the system in time.
The code snippets in this section are taken from the minimal new-interface example so that the tutorial code and the runnable example stay in sync. The snippets are included by named code markers instead of fixed line numbers, so the documentation follows the example when code is moved inside the file.
Describe the Crystal Geometry
In order to simulate laser pump and ASE behavior, HASEonGPU first needs a geometry for the laser crystal. The new interface calls this geometry a Topology: it contains the transverse mesh, the triangle connectivity, and the z-levels that are used to extrude the 2D mesh into prism cells.
topology = MeshTopology.fromGrid(
Grid(xExtent=4, yExtent=4, zExtent=0.7, tileSizeX=0.25, tileSizeZ=0.7 / 9.0)
)
Here Grid describes a rectangular crystal. xExtent and yExtent are
the transverse size of the mesh. zExtent is the crystal length in the
propagation direction. tileSizeX and tileSizeY control the transverse
mesh spacing, while tileSizeZ controls the spacing between z-levels.
MeshTopology.fromGrid(...) triangulates that rectangular grid and keeps the
z-level information needed later by the gain medium. Since HASEonGPU currently does not support 3 dimensional input data,
but rather infers three dimensionality by extruding a 2 dimensional mesh given the number of layers and the physical-distance
called “thickness” between the layers. The same topology object
can also be created from 2 dimensional point clouds, planar STL files, or gmsh triangle
meshes; see Topology.
Attach Material and State Data
A topology only describes the dimensions and geometrical properties of the crystal. The GainMedium adds what the crystal is made of and what state it is currently in. This is where the population inversion, cladding, surface data, doping density, and fluorescence lifetime are assigned.
medium = GainMedium(topology=topology)
print(medium.get("betaCells").expectedShape)
medium.withPhysicalProperties(
betaCells=np.zeros(medium.get("betaCells").expectedShape),
claddingCellTypes=np.zeros(medium.get("claddingCellTypes").expectedShape, dtype=np.uint32),
refractiveIndices=[2, 1, 3, 4],
reflectivities=np.zeros(medium.get("reflectivities").expectedShape),
nTot=1.388e20 * 2.0, # Dotierungsdichte [1/cm^3]
crystalTFluo=9.41e-4, # fluorescence lifetime
claddingNumber=1,
claddingAbsorption=5.5, # [1/cm]
)
The shape of most arrays depends on the mesh, so the example asks the medium
for the expected shape before allocating data. For example,
medium.get("betaCells").expectedShape returns
(numberOfPoints, numberOfLevels). reflectivities returns
(2, numberOfTriangles) because there is one value for the bottom surface
and one for the top surface of every triangle.
The data layout remains compatible with the low-level arrays documented in Legacy Python Interface.
The properties in this minimal setup are:
betaCellsExcited-state fraction \(\beta_i\) at mesh points and z-levels. The time integration updates this array.
claddingCellTypesTriangle-wise cladding labels. In an extruded 2D mesh this is how side regions can be assigned to cladding groups.
refractiveIndicesRefractive indices for the material transitions at the lower and upper crystal surfaces. The layout is
[bottomInside, bottomOutside, topInside, topOutside]and is used when reflections are enabled.reflectivitiesSurface reflectivity per triangle. Row 0 describes the bottom surface and row 1 describes the top surface.
nTotActive-ion concentration \(N_{\mathrm{tot}}\) in the gain medium. Pump absorption, emission, and ASE depletion are scaled by this density.
crystalTFluoFluorescence lifetime \(\tau\). The time loop uses it for spontaneous decay of the excited population.
claddingNumberandcladdingAbsorptionSelect which triangle label is treated as cladding and how strongly that cladding absorbs.
See GainMedium and the low-level argument reference in Legacy Python Interface for the exact layouts passed to HASEonGPU.
Provide Absorption and Emission Spectra
The pump and ASE calculation need material cross sections. The pump uses the absorption cross section \(\sigma_a\) at the pump wavelength \(\lambda\) to update the excited population. The ASE calculation uses the absorption and emission spectra, \(\sigma_a(\lambda)\) and \(\sigma_e(\lambda)\), to compute wavelength-dependent amplification and loss. This enables multichromatic ASE calculation.
cross_sections_data = SpectralDecomposition(
wavelengthsAbsorption=[900.0, 910.0],
crossSectionAbsorption=[1.1e-21, 1.2e-21],
wavelengthsEmission=[1020.0, 1030.0],
crossSectionEmission=[2.0e-20, 2.48e-20],
resolution=2,
)
SpectralDecomposition stores these tables in one object. Each wavelength
array must have the same length as its matching cross-section array:
wavelengthsAbsorption with crossSectionAbsorption and
wavelengthsEmission with crossSectionEmission. The resolution value
is passed to the ASE backend as the spectral interpolation resolution.
Describe the Pump
The pump defines how external radiation changes betaCells
(\(\beta_i\)) during a time-dependent simulation. HASEonGPU separates the
radiation description from the solver: PumpProperties stores spectra, beam
parameters, a solver object, and any custom values that a solver needs.
The minimal example intentionally uses a toy custom solver to demonstrate the extension point rather than a physical pump model:
pump = PumpProperties(
spectralProperties=cross_sections_data,
intensity=16e3, # [W/cm^2]
pumpSubsteps=100,
wavelength=940e-9, # [m]
solver=MyPumpSolver(),
radiusX=1.5,
radiusY=1.5,
superGaussianOrder=40,
myCustomVar=6
)
A custom pump solver only needs a step(input, pump) method. It receives
the current beta array \(\beta\) as input["betaCell"] and returns an
updated beta array with the same shape. pump.getProperty(...) is how
custom solver parameters are read:
class MyPumpSolver:
def step(self, input, pump):
beta = input["betaCell"]
mycustom = pump.getProperty("myCustomVar")
pump.withProperty("myCustomVar",mycustom+1)
return np.ones_like(beta) - beta
For physical pumping, use one of the built-in solvers. The continuous
OneDimensionalZTraversal solver takes a PumpRadiationProfile and
computes \(d\beta/dt\) from the local pump intensity, material cross
sections, and photon flux:
from HASEonGPU import (
OneDimensionalZTraversal,
PumpProperties,
PumpRadiationProfile,
)
profile = PumpRadiationProfile(
intensity=16e3,
wavelengths=[940e-9],
waist=(1.5, 1.5),
propagationDirection=(0.0, 0.0, 1.0),
superGaussianOrder=40,
backReflection=True,
reflectivity=1.0,
)
pump = PumpProperties(
crossSections=cross_sections_data,
profile=profile,
solver=OneDimensionalZTraversal(),
)
If no solver is supplied, Simulation uses the legacy
BetaIntegrationGaussianSolver. Both built-in solvers support a
super-Gaussian transverse profile, but they differ in time treatment: the
legacy solver integrates an analytical update over pumpSubsteps, while the
continuous solver evaluates an instantaneous pump rate once per derivative
evaluation. See PumpProperties and Pumping for the detailed
physics and parameter reference.
Configure PhiASE
At this point the geometry, material state, spectra, and pump are known. The
remaining question is how the ASE calculation should be executed.
To answer that question, the interface provides a PhiASE object, which is a configuration object for the HASEonGPU ASE
C++ backend, which sets sampling limits, adaptive convergence settings, reflection handling,
backend name, and parallel execution mode.
phi_ase = PhiASE(
spectralProperties=cross_sections_data,
minRaysPerSample=1000,
maxRaysPerSample=1000,
repetitions=1,
adaptiveSteps=1,
mseThreshold=0.005,
useReflections=True,
backend="Host_Cpu_CpuSerial",
parallelMode="single",
numDevices=1,
)
minRaysPerSample and maxRaysPerSample bound the Monte Carlo ray count
per sample, the \(N\) used in the estimator in the scientific background.
mseThreshold, repetitions, and adaptiveSteps control how adaptive
sampling can increase the accuracy of the Monte Carlo integration.
useReflections enables the surface reflection model that
uses reflectivities from the gain medium.
backend, parallelMode, and numDevices describe how HASEonGPU should
run. When phi_ase.run(...) is called, PhiASE builds the low-level host
mesh and compute parameter objects and forwards them to the compiled
HASEonGPU implementation. PhiASE can also be used to execute a one-shot ASE call without a time loop.
This is shown in PhiASE. Generated signatures and member lists are available in the Python API Reference.
The backend value is a runtime selection string, not a fixed Python enum.
The minimal example uses "Host_Cpu_CpuSerial" because that backend is
available in a plain CPU build, but GPU and threaded CPU builds can expose
additional names. The Python interface exposes the CMake-built backend-name
library through AlpakaBackends so scripts can query the names supported by
the installed build:
from HASEonGPU import AlpakaBackends
print(AlpakaBackends.all())
backend = AlpakaBackends.Host_Cpu_CpuSerial
Pass one of these strings to PhiASE(..., backend=...). The same backend
names are used by the low-level calcPhiASE(...) interface and by the
--backend= option of the command-line binary. See
Backend Selection for build-time backend selection,
runtime backend naming, and troubleshooting the backend-name helper library.
Assemble the Time Simulation
Simulation connects the objects above into a time-dependent calculation.
It owns the current time, evaluates the pump contribution, calls PhiASE for
ASE when enabled, combines pump gain, ASE depletion, and fluorescence decay
into \(d\beta/dt\), and advances betaCells with the selected time
integration method.
simulation = Simulation(
gainMedium=medium,
pump=pump,
phiASE=phi_ase,
timeIntegrationSolver=RungeKutta4(),
timeStep=1e-5,
endTime=1e-3,
)
simulation.onInit(initFunc)
simulation.onStep(printState)
simulation.onStep(writeVtkState, "minimal_phi_ase_{step:03d}.vtk")
simulation.runSteps(3)
# Equivalent long run:
# simulation.runUntil(endtime=1e-3)
RungeKutta4 is the time integration solver in this example. Other built-in
solvers and the custom solver protocol are listed in
Utilities.
The three callback registrations are optional but useful:
onInitRuns once before the first step and receives the
Simulationobject.onStep(printState)Runs after every completed step and receives a
TimeStepStatesnapshot.onStep(writeVtkState, "phi_{step:03d}.vtk")Registers a user callback.
Simulationpasses theTimeStepStateas the first argument and the filename as the second, so the callback can callvtkWedge(filename, state)or compute derived fields before writing.
One simulation step runs onInit once, then beforeStep callbacks. The
time-integration solver evaluates the derivative as needed; each derivative
evaluation updates betaVolume, runs ASE if enabled, computes
dndtAse and dndtPump, and adds fluorescence decay. After integration,
Simulation clips beta to [0, 1], refreshes betaVolume, stores the
latest TimeStepState, and runs onStep callbacks.
simulation.runSteps(3) runs exactly three steps. Pass
pumpSteps to limit pump action to the first outer simulation steps while
ASE and fluorescence continue, for example simulation.runSteps(150,
pumpSteps=50). The same value may be stored on PumpProperties when the
pump schedule belongs with the pump setup. pumpSteps is distinct from
PumpProperties.pumpSubsteps: pumpSubsteps is only the internal pump
integration resolution inside one pumped outer step. For a time-based run use
simulation.runUntil(endTime=1e-3) or set endTime in the constructor and
call simulation.runUntil().
Set prePump=True on Simulation to run the first outer step without ASE.
This keeps pump and fluorescence active, then enables ASE normally from the
second step onward. The option is useful when a run should seed betaCells
before the first expensive ASE transport solve.
Passing PhiASE Settings from YAML
The Python interface can load the ASE compute settings from a YAML file. This is useful when the physical setup is assembled in Python, but sampling, backend, MPI, and output settings should stay in a small run configuration.
phiASE:
min_rays_per_sample: 100000
max_rays_per_sample: 1000000
mse_threshold: 0.05
repetitions: 2
adaptive_steps: 4
use_reflections: true
monochromatic: false
compute:
backend: Host_Cpu_CpuSerial
parallel_mode: single
numDevices: 1
write_vtk: false
phi_ase = PhiASE.fromYaml(
"phi_ase.yaml",
spectralProperties=spectra,
)
simulation = Simulation(
gainMedium=medium,
pump=pump,
phiASE=phi_ase,
timeIntegrationSolver=RungeKutta4(),
timeStep=1e-5,
)
PhiASE.fromYaml(...) accepts the same keyword overrides as the constructor.
Overrides are applied after reading the file, so they are the right place for
objects that cannot be represented by the YAML settings, such as
spectralProperties. Keys can be written in the Python attribute style
(minRaysPerSample) or in common snake-case style
(min_rays_per_sample). The same settings may be placed at the YAML top
level or grouped under phiASE, phi_ase, experiment, or compute.
The YAML file configures PhiASE only. Geometry, gain-medium arrays,
spectral tables, pump properties, and the time-integration solver are still
normal Python objects. This keeps mesh parsing and array validation in the
typed Python interface while letting run-control parameters live in a
configuration file. See PhiASE for the complete list
of accepted keys and command-line helper support.
Inspect Results
last_state = simulation.getLastState()
print(f"last completed step: {last_state.step}")
simulation.getLastState() returns the most recent TimeStepState. The
state contains the completed step index, physical time, betaCells,
betaVolume, phiAse (\(\Phi_i\)), pump derivative, ASE derivative,
and the raw ASE result object. Simulation keeps only this latest snapshot
in memory. Use onStep callbacks to write, inspect, or store every step.