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:
python3 -m pip install -e .
This installs the Python package in editable mode and also installs the Python
dependencies declared in pyproject.toml.
Source-Tree Import
If the wheel installation above does not work, you can use the source tree directly as a temporary workaround.
Place or run your script inside the repository root. From there, Python should be
able to import HASEonGPU directly without installing the package.
For this workaround the dependencies have to be installed manually:
python3 -m pip install -r requirements.txt
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.
In general the data-layout is still in-line with the pythonInterfaceLegacy 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 mult-chromatic 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 energy is deposited into betaCells
(\(\beta_i\)) during one time step. In the minimal setup the pump is a
super-Gaussian beam with wavelength \(\lambda\), intensity \(I\),
radii, and exponent. It uses the same spectral data object created above.
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,
exponent=40,
myCustomVar=6
)
PumpProperties is intentionally flexible. Standard pump parameters such as
intensity, wavelength, radiusX, radiusY, and exponent are
read by the default pump routine. Additional keyword arguments are stored as
custom properties and can be read by user-defined pump solvers.
The default solver is BetaIntegrationGaussianSolver. It evaluates the
super-Gaussian intensity profile \(I(x, y)\) at each transverse topology
point, propagates the pump through the z-levels (one-dimensional pump), optionally adds back
reflection, and returns the beta distribution \(\beta\) after pumping.
The example passes a custom solver to show the extension point:
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
A custom pump solver only needs a step(input, pump) method. It receives
the current beta array \(\beta\) as input["betaCell"] and returns the
updated beta array with the same shape. pump.getProperty(...) is how
custom solver parameters are read. More realistic pump construction is covered in
PumpProperties.
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, runs a pump routine, calls PhiASE for ASE,
combines pump gain, ASE depletion, and fluorescence decay into a derivative
called dndtASE (\(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(vtkWedge("minimal_phi_ase_{step:03d}.vtk", medium))
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(vtkWedge(...))Writes
phiAse(\(\Phi_i\)) to VTK files for visualization. The filename template can use values from the state, such as{step:03d}.
One simulation step performs the physical update in this order: pump contribution, ASE calculation, fluorescence decay, time integration, beta volume update, result storage, and step callbacks.
simulation.runSteps(3) runs exactly three steps. For a time-based run use
simulation.runUntil(endTime=1e-3) or set endTime in the constructor and
call simulation.runUntil().
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
results = simulation.getResults()
print(f"stored {len(results)} time-step states")
simulation.getResults() returns the stored TimeStepState objects. Each
state contains the completed step index, physical time, betaCells,
betaVolume, phiAse (\(\Phi_i\)), pump derivative, ASE derivative,
and the raw ASE result object. These states are the main Python-side output
of the time loop.