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:

  1. build a topology

  2. attach material/state arrays with GainMedium

  3. provide spectra

  4. define pump properties

  5. configure PhiASE

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

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.