Python Interface Guide
The Python interface is the recommended way to build HASEonGPU simulations. It keeps the physical setup in Python objects and uses the compiled C++ backend for the ASE calculation.
The usual workflow is:
build a topology
attach material/state arrays with GainMedium
provide spectra
define pump properties
configure PhiASE
run a Simulation
For generated signatures and member lists, use the Python API Reference.
Installation
Install from the repository root after following Getting Started:
python3 utils/configure_hase.py
# run the printed command, typically:
CMAKE_ARGS="<selected CMake options>" python3 -m pip install -v .
pip install -v . builds the standalone C++ backend and installs the Python
frontend with private runtime helpers. The configurator chooses compatible openPMD provider settings,
prints the install command, and writes the optional
config/hase-phiase.yaml compute-settings file.
If you change the compiler or C++ runtime used for the extension, rebuild and reinstall:
python3 -m pip install --force-reinstall --no-cache-dir -v .
Concept Pages
Python interface guide
Minimal Example Tutorial
The snippets below come from example/minimalExampleNewInterface.py and stay
in sync with that runnable example.
Geometry
Describe the transverse mesh and z-levels. HASEonGPU currently uses a 2D triangular mesh extruded into prism layers.
topology = MeshTopology.fromGrid(
Grid(xExtent=4, yExtent=4, zExtent=0.7, tileSizeX=0.25, tileSizeZ=0.7 / 9.0)
)
Grid is the shortest path for rectangular media. MeshTopology can also
be created from point clouds, planar STL files, legacy VTK wedge files, and
gmsh triangle meshes; see Topology.
Material and State
Attach physical arrays and scalar material properties to the topology:
medium = GainMedium(topology=topology)
print("betaCells shape:", medium.get("betaCells").expectedShape)
for point in medium.getPoints():
point.betaCells = 0.0
for prism in medium.getPrisms():
prism.betaVolume = 0.0
for triangle in medium.getTriangles():
triangle.claddingCellTypes = 0
triangle.reflectivities = [0.0, 0.0]
medium.get("refractiveIndices").value = np.asarray([2.0, 1.0, 3.0, 4.0], dtype=np.float32)
medium.get("nTot").value = 1.388e20 * 2.0 # Doping density [1/cm^3]
medium.get("crystalTFluo").value = 9.41e-4 # Fluorescence lifetime [s]
medium.get("claddingNumber").value = 1
medium.get("claddingAbsorption").value = 5.5 # [1/cm]
class ThermalPrism(PrismSchema):
temperature = PrimitiveFieldSpec(
"temperature", "custom_temperature", np.float64, unit="K", backendRequired=False
)
medium.withPrimitiveSchema(ThermalPrism)
for prism in medium.getPrisms():
prism.temperature = 300.0
first_prism = next(iter(medium.getPrisms()))
print("prism fields:", first_prism.getFields())
for field in first_prism.getFields():
if field.name == "temperature":
field.value(305.0)
print("first prism temperature:", first_prism.temperature)
Use medium.get("...").expectedShape when allocating mesh-dependent arrays.
The most important fields are betaCells for point-level excited-state
fraction, betaVolume for prism-centered beta, cladding labels,
reflectivities, active-ion density nTot, and fluorescence lifetime
crystalTFluo. See GainMedium for field shapes and
openPMD metadata.
Spectra
Provide absorption and emission cross sections for pump and ASE calculations:
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,
)
print("spectral fields:", cross_sections_data.getFields())
Each wavelength array must match the length of its cross-section array. The
resolution value is passed to the ASE backend as spectral interpolation
resolution.
Pump
PumpProperties stores beam/spectrum inputs, the pump solver, and optional
custom solver parameters.
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 solver needs a step(input, pump) method that receives the current
betaCell array and returns an updated array of the same shape:
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 a built-in solver such as
OneDimensionalZTraversal with a PumpRadiationProfile. If no solver is
supplied, Simulation uses BetaIntegrationGaussianSolver. Details are in
PumpProperties and Pumping.
PhiASE
PhiASE configures the ASE backend: Monte Carlo ray limits, adaptive
sampling, reflections, compute backend, openPMD backend, and parallel 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,
)
Use a backend name reported by the installed build:
from HASEonGPU import AlpakaBackends
backend = AlpakaBackends.all()[0]
backend is the Alpaka compute backend. openpmdBackend is the openPMD
storage/streaming backend such as adios-sst. See
Backend Selection and
openPMD Transport.
Simulation
Simulation combines material state, pump, ASE, and time integration:
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)
Each step applies callbacks, evaluates pump and ASE contributions as required
by the selected time-integration solver, clips beta to [0, 1], updates
betaVolume, stores the latest TimeStepState, and runs onStep
callbacks.
Useful run methods are:
simulation.runSteps(3)
simulation.runSteps(150, pumpSteps=50)
simulation.runUntil(endTime=1e-3)
pumpSteps limits pump action to the first outer simulation steps while ASE
and fluorescence continue. prePump=True on Simulation runs the first
outer step without ASE so the pump can seed betaCells before the first ASE
solve.
YAML Compute Settings
PhiASE.fromYaml(...) can load run-control settings while keeping geometry,
material arrays, spectra, and pump setup in Python:
phiASE:
min_rays_per_sample: 100000
max_rays_per_sample: 1000000
mse_threshold: 0.05
repetitions: 2
adaptive_steps: 4
use_reflections: true
compute:
backend: Host_Cpu_CpuSerial
openpmd_backend: auto
parallel_mode: single
numDevices: 1
phi_ase = PhiASE.fromYaml("config/hase-phiase.yaml", spectralProperties=spectra)
Constructor keyword arguments override YAML values. hase-configure writes a
small YAML file with only the compute settings.
Results
last_state = simulation.getLastState()
print(f"last completed step: {last_state.step}")
simulation.getLastState() returns the latest TimeStepState with step,
time, betaCells, betaVolume, phiAse, pump derivative, ASE
derivative, and the raw ASE result object. Use callbacks to store or export
every step.