Skip to main content

Ultrafast Laser TTM Toolbox: two-temperature model solvers for femtosecond laser heating of metals

Paper DOI DOI CI status PyPI version Latest release MIT License Python 3.10+, NumPy, SciPy, Numba

Python solvers for ultrafast pulsed-laser heating of metals, built on the two-temperature model (TTM) and packaged as laserttm. The toolbox spans single-pulse electron-lattice dynamics on femtosecond timescales, heat accumulation over thousands of pulses, and moving-beam scans. A two-stage solution strategy keeps multi-pulse simulations fast on a laptop, with no MATLAB required.

This is the Python port of the Ultrafast Laser TTM Toolbox, the MATLAB reference implementation for the model published in:

Fieser, D., Dewanjee, U. N., and Hu, A. (2026). A Computationally Efficient Two-Stage Two-Temperature Model for Multi-pulse Femtosecond Laser Heat Accumulation in Tungsten. Journal of Materials Engineering and Performance. doi:10.1007/s11665-026-14738-6

The defaults reflect the tungsten work in the paper, but material presets for W, Cu, Au, and Al plus a custom mode support other metals, pulse widths, spot sizes, repetition rates, and scanning conditions.

Highlights

  • Two-stage multi-pulse strategy. Each pulse period splits into a full electron-lattice TTM solve during the pulse and relaxation, then Crank-Nicolson thermal diffusion for the inter-pulse gap. The depth solvers use a stiff BDF integrator and the 0D solvers use step-identical adaptive RK4. The baseline 50-pulse accumulation run finishes in well under a second.
  • Six solver entry points. 0D surface point, 1D depth-resolved, radial profile, single-pulse visualization, electron-lattice inversion analysis, and a scanning-beam surface model.
  • Validated against the MATLAB reference. Every solver is tested against golden fixtures generated by the MATLAB toolbox. The kernel-based solvers agree to round-off, near 1e-15 relative error, and the stiff solvers agree at integrator tolerance. See Validation.
  • Captures the surface temperature inversion. The lattice runs hotter than the electrons after each pulse. Resolving this requires depth resolution, and it is a focus of the companion paper.
  • Config-dict interfaces. Every solver accepts a plain cfg dict with the same field names and defaults as the MATLAB cfg structs, and returns a results dict with the same shared v1 field contract, so existing studies translate directly.
  • Pure scientific Python. NumPy, SciPy, and Numba, with figures in matplotlib. Kernels compile once and are cached on disk.

All figures below come from the solvers in this repository at their baseline example settings. The generating script is docs/assets/generate_gallery.py.

Electron and lattice surface temperature during one femtosecond pulse in tungsten

Single-pulse electron-lattice dynamics at the tungsten surface, from the 0D solver. The electron bath spikes above 2600 K within the 500 fs pulse, then equilibrates with the lattice through electron-phonon coupling in a few picoseconds.

Multi-pulse heat accumulation in tungsten over 600 pulses

Multi-pulse heat accumulation from the 0D solver, 600 pulses at 5 MHz. The equilibrated and residual surface temperatures climb pulse by pulse as heat arrives faster than it diffuses away.

Peak surface temperature map for a scanning femtosecond laser beam
Scanning-beam peak-temperature footprint at 40 W, 18 MHz, and 1 m/s. Accumulation along the scan carries the peak past tungsten's 3422 °C melt point.
Radial surface temperature profile under a Gaussian femtosecond laser spot
Residual radial temperature profile after 100 pulses under a Gaussian spot.

Getting Started

Requirements: Python 3.10 or newer. NumPy, SciPy, Numba, and matplotlib are installed automatically.

Install the latest release from PyPI:

pip install laserttm              # add "laserttm[mcp]" for the MCP server

Or, for development, clone the repository and install editable:

git clone https://github.com/dfieser/ultrafast-laser-ttm-py.git
cd ultrafast-laser-ttm-py
pip install -e ".[dev]"           # pytest + ruff + the MCP extra

Pin a released version with pip install laserttm==X.Y.Z in environments used for real studies, and keep the editable install for development work only.

Then run a baseline example:

python examples/run_surface_point.py

Or call a solver directly with your own parameters:

from laserttm import surface_point_solver

cfg = {
    "material": "W",            # tungsten preset ('Cu', 'Au', 'Al', 'custom')
    "Pavg": 10,                 # average power [W]
    "spotRadius": 100e-6,       # 1/e^2 spot radius [m]
    "f_rep": 5e6,               # repetition rate [Hz]
    "tau_FWHM": 500e-15,        # pulse width, FWHM [s]
}
cfg["nPulses"] = 50                # simulate 50 pulses

results = surface_point_solver(cfg)

Every run writes its text output under outputs/ and returns a results dict you can inspect or post-process. The first solver call in a fresh environment takes a little longer while Numba compiles the kernels. They are cached on disk after that.

Material properties

Presets for W, Cu, Au, and Al live in materials.py. Any single property can be changed without leaving the preset, so studying one parameter no longer means restating the whole material:

cfg = {
    "material": "W",
    "delta_opt": 23e-9,   # optical penetration depth [m], or alpha_opt in 1/m
}

Overridable fields are gamma, Cl, G, kl, ke0, alpha_opt or delta_opt, and T_melt_C. The ke0 and optical fields apply to the depth-resolved solvers, which are the ones that model absorption along depth. Setting a conductivity replaces tungsten's measured k(T) curve with a constant value, and kTable set to measured or constant overrides that choice. Use material set to custom with the *_manual fields to define a metal from scratch.

Suggested progression:

  1. examples/run_surface_point.py for the simplest pulse-accumulation case
  2. examples/run_depth_profile.py for the main 1D depth-resolved model
  3. examples/run_radial_profile.py for radial spread under a Gaussian spot
  4. examples/run_scanning_beam.py for a moving-beam process

Finding the config fields

Every solver can describe itself, so nothing needs to be looked up in source:

from laserttm import describe_results, describe_solver, validate_config

describe_solver("depth_profile")     # every key with unit, default and range
describe_results("depth_profile")    # every results key with unit and meaning
validate_config("depth_profile", cfg)  # check a config in milliseconds

validate_config reports every problem at once and names the fix. A key with the wrong case, a key belonging to a different solver, and a value that looks like microns rather than metres are all caught before a run starts rather than being silently ignored. It also returns the pulse energy, peak and absorbed fluence and pulse count the config implies, and warns when the peak fluence is past the material's single-shot ablation threshold, when simDuration is not a whole number of periods, or when the inter-pulse diffusion step count will be raised.

The MATLAB repo's project wiki covers the model physics. The config fields carry over one-to-one.

Solvers

Solver Module What it computes Best use
Surface point surface_point.py 0D electron and lattice temperatures at the surface, with inter-pulse depth diffusion fastest pulse-accumulation studies and sweeps
Depth profile depth_profile.py 1D depth-resolved Te(z,t) and Tl(z,t), per-pulse peaks, inversion metrics main multi-pulse workflow, resolves the surface inversion
Radial profile radial_profile.py radial surface temperature under a Gaussian spot, in 'scale' or 'independent' mode melt-radius and footprint studies
Single pulse single_pulse.py one pulse with spatial snapshots at chosen delays early-time inspection and teaching figures
Inversion analysis inversion_quantifier.py per-pulse inversion magnitude, onset, and duration statistics quantifying the Tl > Te inversion across pulses
Scanning beam scanning_beam.py 2D surface temperature under a moving beam translating stationary results to a scanned process

All six return a results dict with a shared envelope: solver, solverId, contractVersion, material, nPulses, wallTime_s, outputFile, outputDir, inputConfig, plus resolvedConfig with the config actually in force, materialProps with the resolved material record, warnings, and caseTag. Since 0.4.0 the envelope also carries diagnostics, the same warnings with a machine-readable code each, the melting-point check Tmelt_C, meltDetected and meltPulse, and the derived pulseEnergy_J, peakFluence_J_m2 and absorbedFluence_J_m2. Every array key declares its axes, so describe_results says which axis of a snapshot is depth and which is time. Every key each solver returns is documented in docs/results-contract.md, generated from the same registry describe_results reads. Every MATLAB toolbox field carries over with its original spelling.

Pulse count: nPulses fires a chosen number of pulses. Without it the count is round(simDuration * f_rep) and a fractional last period is coasted but never fired, which validate_config now points out. With nPulses set, simDuration only decides where the last coast ends and defaults to nPulses / f_rep.

Diagnostics: every solver compares its peak lattice temperature against the melting point and its peak fluence against the material's single-shot ablation threshold, tungsten's being 0.44 J/cm^2, and reports both under diagnostics with the codes above_melting and above_ablation. The model has no phase change or ablation, so a run past either limit describes deposited energy, not the material.

Time-resolved output: the depth solver returns its first-pulse depth snapshots at exactly the requested snapshotDelays, keeps residual depth profiles at the pulses named in profileSnapshotPulses, and with enableRadialProfile derives crossSections_C, the radius-by-depth map after each of those pulses. The scanning solver keeps surface maps at mapSnapshotPulses and returns the beam position at every pulse in beamX_m, alongside Celsius twins of its kelvin maps.

Energy conservation: since 0.2.0 the pulse deposit in the surface-point, radial and scanning solvers conserves the absorbed energy on any grid. The MATLAB reference raises the surface node to the post-pulse equilibrium temperature and lets the rise decay over Leff, which on a depth grid coarser than Leff injects roughly (dz/2)/Leff times the intended energy per pulse. At the library defaults that inflated heat accumulation by about 2.5x per pulse, and by an order of magnitude for nanometre-scale deposition layers. Set legacyDeposit: True to reproduce the MATLAB toolbox output exactly, which is how the golden-fixture tests run.

Long runs: the surface-point, depth and radial solvers accept storeHistory: False to drop the per-pulse time histories, which are needed only for the timeline figures and the report's XY table. This bounds memory for accumulation studies past 100,000 pulses while leaving every physical result unchanged. reportHistory: False keeps the history in the results but leaves its table out of the text report, which is otherwise tens of megabytes for a few thousand pulses and takes longer to write than the solve. writeReport: False writes no report at all, and verbose: False keeps the console silent.

Coast resolution: Ndiff, the Crank-Nicolson step count per inter-pulse period, is a floor. The surface-point, depth and radial solvers raise it as needed to hold the coast Fourier number at 0.5 and report the count used as NdiffUsed. The end-of-period residual is unchanged to a few millikelvin either way; what the raise fixes is the samples inside the first 100 ns of each coast, which were off by half at 1 MHz.

Progress popup: the multi-pulse solvers show a waitbar with a live time estimate during interactive runs, matching the MATLAB toolbox. Set showProgress: True or False in the config to force it either way; False also silences the per-pulse progress lines on the console. When unset, the popup appears only for terminal runs with a display, so test suites, batch pipelines, and the MCP server stay headless. Setting the LASERTTM_NO_PROGRESS environment variable also disables it.

Command line

The package installs a laserttm console script for batch and scripted use:

laserttm list                              # solvers, with guidance on which to use
laserttm describe radial_profile           # every config key, unit, default and range
laserttm materials                         # material presets and their properties
laserttm validate study.json               # check a config without running it
laserttm run study.json --dry-run          # validate and report the cost
laserttm run study.json --out results.json # run a config, .npz also supported
laserttm schema radial_profile             # JSON Schema for typed consumers
laserttm version

Configs are validated before a run starts, so a mistyped key stops with a message naming the correction instead of quietly running on defaults. validate and run --dry-run also print the pulse energy, peak fluence and pulse count the config implies, and the runtime estimate now accounts for the pulse width, the history table, and the kernel loading on the first call.

A config file is the solver's cfg dict plus a solver key, with the same field names as the Python and MATLAB interfaces:

{
  "solver": "radial_profile",
  "material": "W",
  "Pavg": 70,
  "f_rep": 40e6,
  "spotRadius": 150e-6,
  "simDuration": 2.5e-5,
  "storeHistory": false
}

Plots are off by default in CLI runs. Pass --plots or set makePlots or saveFigures in the config.

MCP server

Install the mcp extra with pip install laserttm[mcp]. The laserttm-mcp script then serves every solver as Model Context Protocol tools over stdio, so AI assistants can drive simulations directly. Register it with Claude Code:

claude mcp add laserttm -- laserttm-mcp

Because multi-pulse runs can take minutes, the server uses a job pattern. start_run launches a solver in a background worker process and returns a run id, check_run polls status with a log tail, and get_results returns the results summary once the run finishes. Full arrays land in ~/.laserttm/runs/<run_id>/results.npz, and the LASERTTM_RUNS_DIR environment variable overrides that location. run_quick wraps the pattern for short runs, and cancel_run terminates a job.

Three tools exist so a caller gets the config right the first time. list_solvers says which solver answers which question, describe_solver returns every accepted key with its unit, default and valid range and every results key with its axes, and validate_config checks a config in milliseconds, names the fix for anything wrong, and returns the derived pulse energy and fluence. start_run validates before spawning and reports the pulse count and estimated runtime, and run_quick refuses a run that cannot fit its timeout instead of returning a partial answer.

Validation against the MATLAB reference

validation/fixtures/ holds golden fixtures produced by running every solver in MATLAB R2026a, via validation/generate_fixtures.m, at fixed configurations that include the MATLAB repo's baseline examples. The test suite re-runs each configuration in Python and asserts agreement:

  • The solvers built on the hand-rolled, step-identical kernels are surface point, radial profile, and scanning beam. These agree to round-off: relative errors near 1e-15, and at most 1.6e-10 K across a 36,000-pulse scan.
  • The solvers built on a stiff integrator are depth profile, single pulse, and inversion quantifier. These run MATLAB ode15s against SciPy BDF, the same solver family at the same tolerances, and agree at integrator-tolerance level with relative errors near 1e-4 on peak temperatures.
python -m pytest                          # fast suite (~2 min)
python -m pytest -m "slow or not slow"    # + the 36,000-pulse scanning baseline (~9 min)
python validation/benchmark.py   # wall-time comparison vs MATLAB

Performance

Warm-run wall times on the same machine. The MATLAB times were recorded when the fixtures were generated, with details in validation/fixtures/manifest.json:

Case MATLAB Python Speedup
surface_point_baseline, 50 pulses 7.11 s 0.07 s 108×
single_pulse_baseline 1.36 s 0.04 s 32×
radial_profile_baseline, 100 pulses 1.41 s 0.10 s 15×
depth_profile_baseline, 100 pulses 17.60 s 12.9 s 1.4×
scanning_small, 3,600 pulses 29.96 s 22.9 s 1.3×
scanning_baseline, 36,000 pulses 927.2 s 543.6 s 1.7×
inversion_baseline, 20 pulses 4.35 s 6.1 s 0.7×

The depth-solver family currently mirrors the MATLAB integration windows exactly. The source term is identically zero beyond 10·τ from each pulse center, so relaxing the step cap outside that window is a planned optimization for the stiff phase.

Repository Layout

ultrafast-laser-ttm-py/
  src/laserttm/     solver package: shared Numba kernels and six solver modules
  examples/         editable single-run baseline scripts
  tests/            golden-fixture validation suite, run with pytest
  validation/       MATLAB golden fixtures, generator, and benchmark harness
  docs/assets/      banner and gallery figures with their generating script
  outputs/          default destination for generated results, untracked

Documentation

How to Cite

If this toolbox contributes to published work, please cite the article:

@article{fieser2026twostage,
  author    = {Fieser, David and Dewanjee, Unmanaa Nileen and Hu, Anming},
  title     = {A Computationally Efficient Two-Stage Two-Temperature Model for
               Multi-pulse Femtosecond Laser Heat Accumulation in Tungsten},
  journal   = {Journal of Materials Engineering and Performance},
  publisher = {Springer},
  year      = {2026},
  doi       = {10.1007/s11665-026-14738-6},
}

To cite the software itself, use the version DOI from the Zenodo record or the metadata in CITATION.cff. The concept DOI 10.5281/zenodo.22210435 always resolves to the latest release.

@software{fieser_ttm_toolbox_py,
  author = {Fieser, David},
  title  = {Ultrafast Laser TTM Toolbox (Python)},
  year   = {2026},
  doi    = {10.5281/zenodo.22210435},
  url    = {https://github.com/dfieser/ultrafast-laser-ttm-py},
}

The MATLAB reference implementation has its own record under concept DOI 10.5281/zenodo.20389305.

Versioning and Releases

The VERSION file at the repository root is the single source of truth. The package version is read from it at build time through [tool.hatch.version] in pyproject.toml and exposed at runtime as laserttm.__version__. Every push to main is automatically published as a GitHub release. The release workflow bumps the patch number in VERSION, tags the commit, and publishes. Zenodo then archives the release under a new version DOI, and the same workflow builds the sdist and wheel and publishes them to PyPI through Trusted Publishing, with no API tokens stored anywhere. To jump to a new minor or major version, edit VERSION yourself in your push and that exact version is released instead.

License

Released under the MIT License. See LICENSE.

Acknowledgments and Funding

This work was supported by the National Science Foundation under Award No. CMMI-2412544, Collaborative Research: Additive Manufacturing of Crack-Free Tungsten Using Ultrashort Pulsed Lasers. PI: Dr. Anming Hu, Division of Civil, Mechanical, and Manufacturing Innovation, NSF Program: AM-Advanced Manufacturing.

The authors gratefully acknowledge Drs. Yanfei Gao, Wenda Tan, and Seungha Shin for their contributions and collaboration on this project.

Additional support was provided by the University of Tennessee, Knoxville, through a hiring package. D.F. gratefully acknowledges support from the UTK 100 Talented PhD Scholarship.

Support for the Center for Materials Processing from the State of Tennessee and the Tennessee Higher Education Commission is also gratefully acknowledged.

Contributing

Bug fixes, portability improvements, and documentation polish are welcome. See CONTRIBUTING.md for scope guidance.

Metadata

Release files for laserttm 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for laserttm 0.4.0
File Size Uploaded
laserttm-0.4.0.tar.gz 8.9 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for laserttm 0.4.0
File Interpreter ABI Platform
laserttm-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 9.1 MB

Release files / laserttm-0.4.0.tar.gz

Download URL laserttm-0.4.0.tar.gz
Size 8.9 MB
Tags Source
SHA-256 checksum
How to use checksums
2b3194381fdb78eafd3464a17254c232d63d4ad8cfe02acac491ac38b2a0c0f7
BLAKE2b-256 checksum
How to use checksums
a2991d8f34e3eb9ec33ab3fbb9b215687a0866be10d21321401e2677ba514395
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log

Release files / laserttm-0.4.0-py3-none-any.whl

Download URL laserttm-0.4.0-py3-none-any.whl
Size 121.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
430d0dc885c052d642154c2ad30d0d14eb9feca625bb6efce09fe8b14c89b5a9
BLAKE2b-256 checksum
How to use checksums
f4d1cb48df3c14626ad61a6129a43c27c5b45a996e3949c055a6235fbf3ba8be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page