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

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:

betaCells

Excited-state fraction \(\beta_i\) at mesh points and z-levels. The time integration updates this array.

claddingCellTypes

Triangle-wise cladding labels. In an extruded 2D mesh this is how side regions can be assigned to cladding groups.

refractiveIndices

Refractive 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.

reflectivities

Surface reflectivity per triangle. Row 0 describes the bottom surface and row 1 describes the top surface.

nTot

Active-ion concentration \(N_{\mathrm{tot}}\) in the gain medium. Pump absorption, emission, and ASE depletion are scaled by this density.

crystalTFluo

Fluorescence lifetime \(\tau\). The time loop uses it for spontaneous decay of the excited population.

claddingNumber and claddingAbsorption

Select 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:

onInit

Runs once before the first step and receives the Simulation object.

onStep(printState)

Runs after every completed step and receives a TimeStepState snapshot.

onStep(writeVtkState, "phi_{step:03d}.vtk")

Registers a user callback. Simulation passes the TimeStepState as the first argument and the filename as the second, so the callback can call vtkWedge(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.