Examples
The examples are YAML configurations run through simulations/main.py. They are ordered by
increasing complexity: constant properties, then latent heat, then temperature-dependent
properties. This page close with library-mode usage. Reading them in order is recommended.
All commands are issued from the repository root with uv:
uv sync # one-time environment setup
The configurations are in simulations/examples/.
Key concepts
The configuration file
Each example is one YAML file with five top-level sections:
Section |
Describes |
|---|---|
|
compute backend, time step |
|
box size |
|
density, conductivity, heat capacity, latent heat, and phase-change temperatures |
|
beam radius, absorptivity, power, and the path file |
|
which outputs to write, and where |
A value is either a plain number (SI units assumed) or a {value, unit} pair:
dt:
value: 4.0e-6
unit: "s"
01_quickstart.yaml is annotated field by field; Configuration Guide lists every key.
The laser path (G-code)
The laser trajectory is a G-code file under simulations/examples/paths/, selected by
laser.path.file. The parser reads the subset needed to drive a moving heat source:
Command |
Meaning |
|---|---|
|
millimetre units, absolute coordinates |
|
rapid move, laser off (positioning) |
|
laser on at power |
|
linear move at feedrate |
The quickstart path is a single track scanned at 0.8 m/s with a 200 W beam:
G21 ; mm
G90 ; absolute
G0 X0.0 Y1.25 ; move to start, laser off
M3 S200 ; laser on, 200 W
G1 X9.7 Y1.25 F48000 ; scan +x at 48000 mm/min = 0.8 m/s
M5 ; laser off
The absorbed power is the beam power (S) times laser.absorptivity. Longer trajectories chain
several G0/G1 moves.
1. Quickstart
01_quickstart.yaml scans a single laser
track over a coarse cuboid with constant material properties. Latent heat and surface convection
are disabled, leaving evaporative cooling as the only non-linear surface flux. The run takes
about a minute on one CPU core. Its YAML is annotated field by field and serves as the
configuration reference.
uv run python simulations/main.py simulations/examples/01_quickstart.yaml
Results are written to out/<timestamp>_quickstart/:
profiles/— temperature along \(x\), \(y\), \(z\) through the laser spot (text).fields/— the final volume field, readable in ParaView through the.xmffile.slices/— the cross-section image configured byio.slice_planes.logs/simulation.log— per-step metrics (T_surface_max, laser power).
Longitudinal (\(xz\), normal \(y\)) section through the laser spot at the end of the run. The red contour marks the solidus/liquidus.
2. Single track with latent heat
02_single_track.yaml adds the two
effects that example 1 omits: latent heat of fusion (L_f) and bottom-face convection
(h_conv_bottom).
Properties remain constant, obtained by evaluating the temperature-dependent 316L polynomials at
\(T_0 = 293\) K. The domain is small with a fine \(z\)-grid (\(\Delta z \approx 1.25\ \mu\text{m}\)) so
the narrow 316L mushy zone is resolved. The run takes a few minutes on one CPU core.
Note
The 316L thermophysical properties used in these examples (\(\rho\), \(k\), \(c_p\), with separate solid and liquid branches) are taken from Chadwick et al., Acta Materialia 282 (2025) 120482, doi:10.1016/j.actamat.2024.120482.
uv run python simulations/main.py simulations/examples/02_single_track.yaml
Melt-pool section for the single track. The two red contours are the solidus and liquidus.
3. Temperature-dependent properties
03_nonlinear_tdep.yaml lets the
conductivity, density and specific heat vary with temperature (solid and liquid expression
branches blended by the liquid fraction), making the problem non-linear. Each step is solved by a
Picard iteration that also resolves the property correction (see Theory & Physics):
max_picard_iter— iteration cap per step (80here).picard_omega— relaxation factor; larger values converge faster until the iteration destabilises (0.31is near the limit for this case).
uv run python simulations/main.py simulations/examples/03_nonlinear_tdep.yaml
Melt-pool section for the temperature-dependent run. The red line marks the solidus/liquidus.
This is a reduced-grid version of the finite-element comparison case.
4. Library mode
The solver can be driven directly from Python rather than the CLI. An external script builds a
SimulationContext and calls solver.step(...) in a loop, which is useful when embedding the
solver in a larger codebase. Nothing is written to disk.
Installing as a library. To import spectral_galerkin_heat from a project outside this repository,
install it as an editable package into your environment:
uv pip install -e /path/to/spectral_galerkin_heat # or: pip install -e /path/to/spectral_galerkin_heat
Or declare it as an editable source in another uv project’s pyproject.toml:
[project]
dependencies = ["spectral_galerkin_heat"]
[tool.uv.sources]
spectral_galerkin_heat = { path = "/path/to/spectral_galerkin_heat", editable = true }
Either way the import name is spectral_galerkin_heat, and edits to the source take effect without
reinstalling. The full runnable script is
orchestrator.py:
uv run python simulations/examples/orchestrator.py
from spectral_galerkin_heat.core.parameters import SimulationContext
from spectral_galerkin_heat.solvers.spectral import SpectralSolver
from spectral_galerkin_heat.backends import NumpyBackend
# 1. Configuration as a plain dictionary
config = {
"simulation": {"backend": "cpu", "dt": 6e-6, "duration": 6e-5},
"domain": {"size": [0.01, 0.005, 0.0025], "mesh": [64, 32, 16]},
"material": {"rho": 7957.5, "k": 13.851, "Cp": 497.89, "name": "316L"}, # reference 316L values
"laser": {"radius": 60.0e-6, "absorptivity": 0.30, "power_nominal": 200.0,
"path": {"type": "gcode", "file": "linear_track.gcode"}},
"io": {}, # empty: the IOManager is not used
}
# 2. Build the context. config_dir must point to the directory holding the
# paths/ subfolder with the G-code (use an absolute path when running elsewhere).
context = SimulationContext.from_dict(config, config_dir="simulations/examples")
# 3. Instantiate and initialise the solver
solver = SpectralSolver(NumpyBackend()) # get_backend("cupy") for GPU
state = solver.initialize(context)
# 4. Time loop
t, dt, t_end = 0.0, context.num.dt, context.num.t_end
while t < t_end:
state, metrics = solver.step(t, dt)
t += dt
print(f"t = {t:.4e} s | T_max = {metrics.get('T_surface_max'):.1f} K")
Further reading
Every configuration key is documented in Configuration Guide, and the output formats in Output Types.