Simulation
Simulation is the high-level time loop. It combines a GainMedium,
PumpProperties, PhiASE configuration, and a time integration solver.
from HASEonGPU import Simulation, RungeKutta4
simulation = Simulation(
gainMedium=medium,
pump=pump,
phiASE=phi_ase,
timeIntegrationSolver=RungeKutta4(),
timeStep=1e-5,
endTime=1e-3,
)
Running
Run a fixed number of steps:
simulation.runSteps(3)
Limit pumping to the first outer simulation steps while ASE and fluorescence continue for the full run:
simulation.runSteps(150, pumpSteps=50)
pumpSteps counts outer calls to Simulation.step(). You can pass it
to runSteps or store it on PumpProperties and then call
simulation.runSteps(150). When neither location provides pumpSteps, the
pump is active for every step passed to runSteps. This is different from
PumpProperties.pumpSubsteps, which only controls the internal time
resolution of the legacy pump integration inside one pumped simulation step.
Seed the first step with pump only:
simulation = Simulation(
gainMedium=medium,
pump=pump,
phiASE=phi_ase,
timeIntegrationSolver=FrozenPhiAseRungeKutta4(),
timeStep=2e-5,
prePump=True,
)
prePump=True skips the ASE backend during the first outer step only. The
pump and fluorescence terms still run, so the first stored state can seed
betaCells before ASE depletion starts on the following steps.
Run until a target time:
simulation.runUntil(endTime=1e-3)
If endTime was supplied to the constructor:
simulation.runUntil()
Run exactly one step and inspect the returned state:
state = simulation.step()
print(state.step, state.time, state.betaCells.shape, state.phiAse.shape)
Simulation Step Order
Each call to step() performs:
One-time
onInitcallbacks.beforeStepcallbacks.Time integration of the beta derivative \(d\beta/dt\). The selected time-integration solver may evaluate this derivative more than once.
During each derivative evaluation,
SimulationupdatesbetaVolume, callsphiASE.run(...)when ASE is enabled for the current step, computes the pump rate through the configured pump solver, and combines pump, ASE, and fluorescence decay. Higher-order solvers can therefore call the ASE backend more than once per outerstep()unless they explicitly freezephiAse.Clipping of updated beta values to
[0, 1].Final
betaVolumeupdate frombetaCells.Latest-state update and
onStepcallbacks.
Callbacks
Lifecycle hooks always insert the simulation-provided object as the first
argument. Extra arguments passed during registration are appended after that
first argument. Callback return values are ignored, and each registration method
returns self so calls can be chained.
onInit(callback, *args, **kwargs)Runs once before the first step. The callback signature is
callback(simulation, *args, **kwargs)and receives the liveSimulationobject first. Use it to initialize or normalize mutable simulation inputs.beforeStep(callback, *args, **kwargs)Runs before every step, after
onInithas run. The callback signature iscallback(simulation, *args, **kwargs)and receives the liveSimulationobject first. Use it for controlled pre-step changes such as time-dependent pump settings.onStep(callback, *args, **kwargs)Runs after every completed step. The callback signature is
callback(state, *args, **kwargs)and receives the completedTimeStepStatefirst. Use it for logging, inspection, exporting, or storing result snapshots.
Examples:
def initialize(simulation, beta0):
simulation.gainMedium.get("betaCells").value = beta0
def adjust_pump(simulation, scale):
simulation.pump.withProperty("scale", scale)
def write_state(state, output_dir):
print(state.step, state.time, state.betaCells.mean())
simulation.onInit(initialize, beta0)
simulation.beforeStep(adjust_pump, 0.5)
simulation.onStep(write_state, output_dir)
This means simulation.onStep(write_state, output_dir) calls
write_state(state, output_dir) for every completed step.
Results
getLastState() returns the most recent TimeStepState snapshot:
last = simulation.getLastState()
last.betaCells.shape
last.betaVolume.shape
last.phiAse.shape
Simulation keeps only the latest state in memory. Use onStep to write,
inspect, or store state for every completed step. getResults() is retained
as an alias for getLastState().
TimeStepState fields are:
step: completed step index.time: simulation time after the step.betaCells: point and level beta values \(\beta_i\).betaVolume: prism beta values \(\beta_j\) used by ASE.phiAse: ASE flux values \(\Phi_i\), orNoneif unavailable.dndtAse: ASE derivative contribution to \(d\beta/dt\).dndtPump: pump derivative contribution to \(d\beta/dt\).aseResult: raw lower-level ASE result object.topology: static mesh topology used by geometry-aware callbacks such as VTK export.
Properties
simulation.time
simulation.stepIndex
time and stepIndex expose the current simulation clock and completed
step count.
Spectral Properties Resolution
Simulation requires spectral properties such as cross sections and emission
spectra. These properties may be defined at different levels, for example
globally for the whole simulation or locally for individual objects.
When multiple definitions are available, the simulation resolves them using a fixed priority order. More specific definitions override more general ones. This determines which spectral data is used for each object during the simulation.
Simulation.crossSectionsphiASE.spectralPropertiesphiASE.crossSectionspump.spectralPropertiespump.crossSections
The resolved spectra are also written back to phiASE if needed.
Time Integration
timeIntegrationSolver must provide:
step(rhs, betaCells, time, timeStep)
Built-in solvers are documented in Utilities.
FrozenPhiAseRungeKutta4 is a special simulation-aware solver: it runs
phiASE once at the beginning of the outer step and keeps that ASE flux
constant for all RK4 stages. Pump and local terms are still evaluated for each
stage trial beta value.
Beta Volume Mapping
Simulation updates betaVolume (\(\beta_j\)) from betaCells
(\(\beta_i\)) after each step using LegacyGridDataBetaVolumeMapper.
The mapper interpolates beta values from topology points and z-levels to prism
centers. It requires scipy.