tracketch
tracketch – simulation of ion tracks in CR-39 nuclear track detectors.
- class tracketch.TrackSimulator(particle_name: str, start_energy_MeV_u: float, etch_model: EtchRateModel | None = None, RDD_name: str = 'Cucinotta', arrival_time_method_name: str = 'dijkstra_numba', dijkstra_connectivity: int = 8, rz_lims_dict: dict | None = None, r_min_um: float | None = None, r_max_um: float | None = None, z_max_um: float | None = None, n_points_r: int | None = None, n_points_z: int | None = None, material_name: str = 'CR39', stopping_power_source: str = 'SRIM', n_straggling_sigma: int = 1, n_uniform_multiplier: int = 3, n_jobs: int = 1, log_level: int | str | None = None, normalise_to_LET: bool = False, logscale_r: bool = True, theta_deg: float = 0.0)[source]
Bases:
objectSimulate chemical etching of a single ion track in CR-39.
The simulation proceeds in three stages:
Dose map – radial dose distribution (RDD) integrated along the ion path, accounting for energy loss and optional range straggling.
Etch-rate map – the calibrated
etch_modelconverts local dose to local etch velocity, with optional debris-damping correction.Arrival-time map – shortest-time wavefront propagation (Dijkstra or Fast Marching) from the detector surface into the bulk.
From the arrival-time map, iso-time contours, track radii, and track depths are extracted for any desired etching duration.
- Parameters:
particle_name (str) – Ion species identifier in the form
"<A><symbol>", e.g."12C","1H","56Fe","238U". Whenstopping_power_source="SRIM"(default) the particle must be one oftracketch.SRIM_PARTICLES; switch tostopping_power_source="libamtrack"to use any nuclide recognised by libamtrack.start_energy_MeV_u (float) – Kinetic energy at the detector surface in MeV per nucleon.
etch_model (tracketch.etching.etch_rate_model.EtchRateModel or None) – Calibrated dose -> etch-rate model. If None, a default model is loaded (
"Doerschel_etching").RDD_name (str) – Radial dose distribution model name (default
"Cucinotta").arrival_time_method_name (str) – Wavefront algorithm:
"dijkstra"(default, auto-selects fastest available backend),"dijkstra_cpp","dijkstra_numba", or"fmm".dijkstra_connectivity (int) – Neighbour connectivity for Dijkstra: 8, 16, or 32.
r_min_um (float or None) – Minimum radial coordinate in um (default 1e-4).
r_max_um (float or None) – Maximum radial coordinate in um (default 20).
z_max_um (float or None) – Maximum depth coordinate in um (default 40).
n_points_r (int or None) – Number of radial grid points (default 400).
n_points_z (int or None) – Number of depth grid points (default 100).
rz_lims_dict (dict or None) – Legacy grid specification dict. Individual keyword arguments above take precedence over keys in this dict when both are supplied.
material_name (str) – Target material –
"CR39"(default) or"water".stopping_power_source (str) – Stopping-power database:
"SRIM"(default, tabulated data for common ions) or"libamtrack"(supports any nuclide, uses the PSTAR/ASTAR parametrisation).n_straggling_sigma (int) – Number of longitudinal-straggling sigma to include (0 = none).
n_uniform_multiplier (int) – Resolution multiplier when regridding to uniform spacing for FMM.
n_jobs (int) – Number of parallel workers for the RDD/dose-map computation.
-1uses all available CPU cores (viajoblib). Parallel execution benefits large grids (n_points_r x n_points_z > ~50 000) orstopping_power_source="libamtrack". For the default grid the inter-process overhead outweighs the gain; keepn_jobs=1(sequential) unless you have increased the grid resolution. Default1.log_level (int or str or None) – Set the logging level for the
tracketchlogger, e.g.logging.DEBUG,logging.INFO, or the string"DEBUG". When None (default) the logger is left untouched.
- dose_map
Local dose in Gy.
- Type:
ndarray, shape (n_z, n_r)
- etch_rate_map
Local etch velocity in um/hr (with debris damping applied, if enabled).
- Type:
ndarray, shape (n_z, n_r)
- etch_rate_map_nodebris
Local etch velocity in um/hr before any debris damping. Identical to
etch_rate_mapwhen debris damping is disabled.- Type:
ndarray, shape (n_z, n_r)
- arrival_time_map
Shortest etchant arrival time in hours.
- Type:
ndarray, shape (n_z, n_r)
- energy_profile_MeV_u
Kinetic energy vs. depth (with straggling).
- Type:
ndarray
- LET_profile_keV_um
Linear energy transfer vs. depth (with straggling).
- Type:
ndarray
- etch_model
The active dose -> etch-rate model.
Notes
Track radius and depth
After construction, extract the observable track geometry for any etching duration with
get_track_radius_um()andget_track_length_um():sim = TrackSimulator(particle_name="12C", start_energy_MeV_u=270.0) radius_um = sim.get_track_radius_um(etch_time_h=3.0) depth_um = sim.get_track_length_um(etch_time_h=3.0) # pit depth below surface
Both return
float("nan")when the track is not yet open (too short an etching time) or when the ion stops before reaching the detector surface.Track contour
For the full 2-D pit shape use
get_iso_time_contour(), which returns(r, z)arrays in um:r, z = sim.get_iso_time_contour(etching_time_h=3.0) import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot(r, z) ax.plot(-r, z) # mirror for full axisymmetric view ax.invert_yaxis() ax.set_aspect("equal")
Uncertainties
Uncertainties are propagated from the calibrated v(d) anchor-velocity uncertainties. These must first be estimated from a calibration dataset using
calibration.lib_optimiser.estimate_parameter_uncertainties()and then stored on the etch model:from calibration.lib_optimiser import estimate_parameter_uncertainties result = estimate_parameter_uncertainties( etch_model=sim.etch_model, models_dict=my_calibration_data, # same dict used during fitting ) sim.etch_model.anchor_velocity_uncertainties_um_h = ( result["anchor_velocity_uncertainties_um_h"] ) sim.etch_model.V_max_uncertainty_um_h = result.get("V_max_uncertainty_um_h")
Once the uncertainties are attached, use the
_with_uncertaintyvariants:radius_um, radius_unc_um = sim.get_track_radius_um_with_uncertainty( etch_time_h=3.0, # etching duration n_sigma=1.0, # propagate +/-1 sigma of v(d) anchors ) result = sim.get_track_length_um_with_uncertainty( etch_time_h=3.0, n_sigma=1.0, ) if result is not None: depth_um, depth_unc_um = result print(f"radius = {radius_um:.2f} +/- {radius_unc_um:.2f} um") print(f"depth = {depth_um:.2f} +/- {depth_unc_um:.2f} um")
Both methods return
nanuncertainties when a bound contour cannot be extracted.get_track_radius_um_with_uncertainty()also floors the uncertainty at half the minimum radial grid step, since the contour extraction cannot resolve sub-cell differences.For a full uncertainty band around the track contour see
get_iso_time_contour_with_uncertainty().- calculate_RDD_Gy(E_MeV_u: float) ndarray[source]
Calculate the radial dose distribution at a given energy.
- Parameters:
E_MeV_u (float) – Kinetic energy in MeV per nucleon.
- Returns:
Dose in Gy at each radial grid point.
- Return type:
ndarray
- get_iso_time_contour(etching_time_h: float) tuple[ndarray, ndarray][source]
Extract the iso-time contour from the arrival-time map.
- get_iso_time_contour_nodebris(etching_time_h: float) tuple[ndarray, ndarray][source]
Iso-time contour computed from the etch-rate map without debris damping.
Useful for comparing predicted track shapes with and without the debris-transport correction. The arrival-time map is computed on demand from
etch_rate_map_nodebriseach time this method is called (not cached).
- get_iso_time_contour_with_normal_uncertainty(etching_time_h: float, n_sigma: float = 1.0, n_band_points: int = 200, min_uncertainty_um: float = 0.15) tuple[tuple[ndarray, ndarray], tuple[ndarray, ndarray], tuple[ndarray, ndarray]] | None[source]
Iso-time contour with uncertainty expressed along local contour normals.
Unlike
get_iso_time_contour_with_uncertainty(which returns a radialr(z)band forfill_betweenx), this method computes the uncertainty envelope perpendicular to the contour itself. This keeps the band visible and geometrically meaningful even where the contour is horizontal.- Return type:
tuple of
((r_c, z_c), (r_lo, z_lo), (r_hi, z_hi))or None.
- get_iso_time_contour_with_uncertainty(etching_time_h: float, n_sigma: float = 1.0, n_band_points: int = 200) tuple[tuple[ndarray, ndarray], ndarray, ndarray, ndarray] | None[source]
Iso-time contour with uncertainty band from v(d) anchor uncertainties.
Computes arrival-time maps for v(d)+/-n_sigma*dV, extracts the three contours, and interpolates the upper/lower bounds onto a common z grid suitable for
ax.fill_betweenx.- Parameters:
- Returns:
tuple of
((r_central, z_central), z_band, r_lo, r_hi)or None.Returns *None (and logs a warning) when the etch model carries no*
anchor velocity uncertainties. Assign the output of
estimate_parameter_uncertaintiestoetch_model.anchor_velocity_uncertainties_um_hfirst.
- get_track_detectability(etch_time_h: float, min_radius_um: float = 0.5, min_depth_um: float = 0.3, min_cone_half_angle_deg: float = 2.0, threshold_percent: float = 5.0) dict[source]
Geometric detectability metrics for a given etching time.
A track is considered detectable when all three criteria are met:
radius_um >= min_radius_umdepth_um >= min_depth_um(pit depth below bulk surface)cone_half_angle_deg >= min_cone_half_angle_deg
- Returns:
Keys:
detected,radius_um,depth_um,cone_half_angle_deg,radius_ok,depth_ok,angle_ok.- Return type:
- get_track_length_um(etch_time_h: float | list, relative_to_surface: bool = True) float | ndarray[source]
Track depth at r = 0 for a given etching time.
- Parameters:
- Returns:
Depth in um, or
nanif the track does not reach r = 0.- Return type:
float or ndarray
- get_track_length_um_with_uncertainty(etch_time_h: float, relative_to_surface: bool = True, n_sigma: float = 1.0) tuple[float, float] | None[source]
Track depth with uncertainty propagated from v(d) anchor uncertainties.
Evaluates the arrival-time map for v(d)+n_sigma*dV and v(d)-n_sigma*dV, reads the depth at r = 0 for each, and returns the central depth together with half the spread as the uncertainty estimate.
- Parameters:
- Returns:
tuple of
(depth_um, depth_uncertainty_um)or None.Returns *None (and logs a warning) when the etch model carries no*
anchor velocity uncertainties.
- get_track_radius_um(etch_time_h: float, threshold_percent: float = 5.0) float[source]
Track opening radius from iso-time contour deviation.
- get_track_radius_um_nodebris(etch_time_h: float, threshold_percent: float = 5.0) float[source]
Track opening radius computed without debris damping.
Convenience wrapper around
get_iso_time_contour_nodebris().
- get_track_radius_um_with_uncertainty(etch_time_h: float, threshold_percent: float = 5.0, n_sigma: float = 1.0) tuple[float, float][source]
Track radius with uncertainty propagated from v(d) anchor uncertainties.
Evaluates the etch-rate and arrival-time maps for v(d)+n_sigma*dV and v(d)-n_sigma*dV, extracts a radius from each, and returns the central radius together with half the spread as the uncertainty estimate.
- Parameters:
- Returns:
radius_um (float) – Central track radius in um.
radius_uncertainty_um (float) – Half-spread of the upper/lower bound radii (1-sigma estimate).
- Raises:
ValueError – If the etch model carries no anchor velocity uncertainties.
- is_track_detected(etch_time_h: float, min_radius_um: float = 0.5, min_depth_um: float = 0.3, min_cone_half_angle_deg: float = 2.0, threshold_percent: float = 5.0) bool[source]
Return True if the track is detectable at this etching time.
Convenience wrapper around
get_track_detectability().
- plot_iso_time_contour(ax: Axes, etching_time_h: float) tuple[ndarray, ndarray][source]
Plot the iso-time contour on ax.
- Returns:
r_coords, z_coords
- Return type:
tuple[ndarray, ndarray]
- plot_map(name: str = 'dose', grayscale_mode: bool = False, plot_contours: bool = True, annotate_figure: bool = True) tuple[source]
Plot the dose, etch-rate, or arrival-time map.
- plot_track_radius(etch_time_h: float, ax: Axes | None = None, show_contour: bool = True, threshold_percent: float = 5.0) tuple[source]
Plot iso-time contour with track-radius indicators.
- Return type:
fig, ax
- recalculate_from_current_etch_model() None[source]
Recompute maps using the current etch model.
Alias for
update_etch_model_and_recalculate(None).
- update_etch_model_and_recalculate(new_etch_model: EtchRateModel | None = None) None[source]
Replace the etch model and recompute etch-rate & arrival-time maps.
The dose map is not recomputed (it depends only on physics, not the etch model).
- Parameters:
new_etch_model (tracketch.etching.etch_rate_model.EtchRateModel or None) – New model. If None, recomputes using the current model (useful after in-place parameter changes).
- class tracketch.EtchRateModel(anchor_doses_Gy: ndarray | list[float] = array([1.00000000e+01, 6.30957344e+02, 3.98107171e+04, 2.51188643e+06, 1.58489319e+08, 1.00000000e+10]), V_bulk_um_h: float = 1.73, V_max_um_h: float | None = None, V_max_uncertainty_um_h: float | None = None, anchor_velocities_um_h: ndarray | list[float] | None = None, anchor_velocity_uncertainties_um_h: ndarray | list[float] | None = None, name: str | None = None, extrapolation_mode: str = 'pchip', debris_alpha: float | None = None, debris_beta: float = 1.0)[source]
Bases:
object- eval_uncertainty_band(dose_Gy: ndarray, n_sigma: float = 1.0, max_sigma_log10: float = 0.5) tuple[ndarray, ndarray] | None[source]
Return lower and upper 1-sigma etch-rate bands at the given doses.
Converts per-anchor velocity uncertainties to log10-velocity space, interpolates them onto
dose_Gywith PCHIP (in log-dose space), and applies the band symmetrically aroundeval(dose_Gy)in log10 space. Results are clipped to[V_bulk, V_max].- Parameters:
dose_Gy (array-like) – Dose values at which to evaluate the band.
n_sigma (float) – Number of standard deviations for the band width (default 1).
max_sigma_log10 (float) – Cap on interpolated sigma in log10-velocity units (default 0.5, i.e. ~factor-of-3 per sigma) to suppress artefacts from poorly-constrained high-dose anchors.
- Returns:
tuple of (lower, upper) ndarrays, or None if no anchor uncertainties
are set on the model.
- get_increments() ndarray[source]
Get relative velocity increments (for optimization).
- Returns:
increments – Relative increments [um/hour], all >= 0
- Return type:
np.ndarray
- get_log_doses() ndarray[source]
Get log10 of dose anchors (for optimization in log-space).
- Returns:
log_doses – log10 of anchor doses [dimensionless]
- Return type:
np.ndarray
- get_log_velocity_increments() ndarray[source]
Get monotonic increments in log10(velocity) space.
- Defines:
u_i = log10(v_i) delta_u_0 = u_0 - log10(V_bulk_um_h) delta_u_i = u_i - u_{i-1} for i >= 1
Since anchor velocities are monotonic and >= V_bulk_um_h, all returned increments are guaranteed to be >= 0.
- Returns:
Non-negative increments in log10(velocity) space.
- Return type:
np.ndarray
- is_monotonic(num_points: int = 256) bool[source]
Check monotonic non-decreasing behavior of v(d) on a dense log-dose grid.
- static linear_increments_to_log(increments: ndarray | list[float], epsilon: float = 1e-06) ndarray[source]
Convert linear increments to log-space (for unconstrained optimization).
Inverse of log_increments_to_linear.
- Parameters:
increments (array-like) – Linear increments (must all be > -epsilon)
epsilon (float, optional) – Small offset used during forward conversion (default: 1e-6)
- Returns:
log_increments – Log-transformed increments
- Return type:
np.ndarray
- static linear_log_velocity_increments_to_optimization(log_velocity_increments: ndarray | list[float], epsilon: float = 1e-12) ndarray[source]
Map positive log-velocity increments to unconstrained optimizer parameters.
- classmethod load_from_json(filepath: str, verbose: bool = False) EtchRateModel[source]
Load an etch rate model from a JSON file.
- Parameters:
filepath (str) – Path to the JSON file
- Returns:
The loaded model
- Return type:
Examples
>>> model = EtchRateModel.load_from_json("my_model.json")
- static log_increments_to_linear(log_increments: ndarray | list[float], epsilon: float = 1e-06) ndarray[source]
Convert log-space increments to linear (for unconstrained optimization).
For optimization, parameterize as: log_increments = log(increments + epsilon) This ensures increments are always positive without constraints.
- Parameters:
log_increments (array-like) – Log-transformed increments (can be any real number)
epsilon (float, optional) – Small offset to ensure positivity (default: 1e-6)
- Returns:
increments – Linear increments, all > epsilon
- Return type:
np.ndarray
- static log_velocity_increments_to_linear(optimization_parameters: ndarray | list[float], epsilon: float = 1e-12) ndarray[source]
Map unconstrained optimizer parameters to positive log-velocity increments.
- plot(dose_range: tuple[float, float] = (1e-06, 10000000000000.0), n_points: int = 500, show_anchors: bool = True, show_v_bulk: bool = True, show_uncertainty_band: bool = True) tuple[Figure, Axes][source]
Plot the etch rate function.
- Parameters:
dose_range (tuple, optional) – (min_dose, max_dose) in Gy for x-axis
n_points (int, optional) – Number of evaluation points
show_anchors (bool, optional) – Whether to show anchor points
show_v_bulk (bool, optional) – Whether to show V_bulk_um_h reference line
show_uncertainty_band (bool, optional) – Whether to show uncertainty band if anchor uncertainties are available
- Returns:
fig, ax
- Return type:
matplotlib figure and axes
- save_to_json(filepath: str) None[source]
Save the etch rate model to a JSON file.
- Parameters:
filepath (str) – Path to the output JSON file
Examples
>>> model.save_to_json("my_model.json")
- update_debris_params(alpha: float | None, beta: float | None = None) None[source]
Update debris-damping parameters.
- update_dose_anchors(anchor_doses_Gy: ndarray | list[float]) None[source]
Update dose anchor positions and recreate spline.
Validates that doses are strictly increasing.
- Parameters:
anchor_doses_Gy (array-like) – New dose anchor points [Gy]. Must be strictly increasing.
- Raises:
ValueError – If doses not strictly increasing or length doesn’t match velocities
- update_from_increments(increments_um_h: ndarray | list[float]) None[source]
Update velocities from relative increments (for optimization).
This is the recommended approach for optimization: optimize the increments directly to guarantee monotonicity by construction.
- Parameters:
increments_um_h (array-like) – Relative increments [um/hour]. Must be all >= 0 and have same length as anchor points.
- Raises:
ValueError – If increments are negative or length doesn’t match If resulting velocities would be < V_bulk_um_h
- update_from_log_velocity_increments(log_velocity_increments: ndarray | list[float]) None[source]
Update anchor velocities from non-negative log10(velocity) increments.
This parameterization is robust for wide dynamic ranges because optimization steps act multiplicatively on velocity.
- Parameters:
log_velocity_increments (array-like) – Increments in log10(velocity) space. Must all be >= 0 and have the same length as anchor points.
- update_velocities(new_velocities_um_h: ndarray | list[float]) None[source]
Update anchor velocities and recreate spline.
Accepts absolute velocities, validates them for monotonicity, and converts to relative increments internally.
Useful during optimization loops.
- Parameters:
new_velocities_um_h (array-like) – New absolute velocity values for anchor points [um/hour]
- Raises:
ValueError – If new velocities violate monotonicity
- tracketch.load_etchrate_model(model_name: str) EtchRateModel[source]
Load a model using the configured path.
- Parameters:
model_name (str) – The model identifier (e.g., ‘Doerschel_etching’)
- Returns:
The loaded model
- Return type:
Examples
>>> model = load_etchrate_model('Doerschel_etching')
- tracketch.save_etchrate_model(model: EtchRateModel, model_name: str) None[source]
Save a model using the configured path.
- Parameters:
model (tracketch.etching.etch_rate_model.EtchRateModel) – The model to save
model_name (str) – The model identifier (e.g., ‘Doerschel_etching’)
Examples
>>> save_etchrate_model(my_model, 'my_custom_model')
- tracketch.get_RDD_Gy(r_m: ndarray, E_MeV_u: float, particle_name: str, RDD_name: str = 'Cucinotta', material_name: str = 'CR39', core_nm: float | None = None, normalise_to_LET: bool | None = None, LET_source: str = 'SRIM', verbose: bool = False) ndarray[source]
Radial dose distribution scaled to material_name.
The RDD is computed for water (via libamtrack) and then corrected:
Radii are density-scaled to account for different electron ranges.
A dose correction restores the radial integral after the range shift.
A stopping-power ratio (SPR) converts dose-to-water into dose-to- target-material.
Optionally, the profile is renormalised so that its radial integral equals the target-material LET.
- Parameters:
r_m (ndarray) – Radial distances in metres.
E_MeV_u (float) – Kinetic energy in MeV/u.
particle_name (str) – Ion species.
RDD_name (str) – RDD model name (default
"Cucinotta").material_name (str) – Target material (default
"CR39").core_nm (float or None) – Core radius override in nm.
normalise_to_LET (bool or None) – Force LET normalisation.
Noneuses the model default.LET_source (str) – Stopping-power source for the SPR (default
"SRIM").verbose (bool) – Print diagnostic information.
- Returns:
Dose in Gy at each radius.
- Return type:
ndarray
- tracketch.get_LET_keV_um(energy_MeV_u: float | ndarray, particle_name: str, material_name: str, source: str = 'SRIM') float | ndarray[source]
Return LET in keV/um from the chosen stopping-power database.
- Parameters:
- Returns:
LET in keV/um.
- Return type:
float or ndarray
- Raises:
ValueError – If source or material_name are not recognised.
- tracketch.get_CSDA_um(energy_MeV_u: float, particle_name: str, material_name: str, source: str = 'SRIM') float[source]
Return the CSDA range in um from the chosen stopping-power database.
- tracketch.convert_MeV_to_MeV_u(Energy_MeV: float, particle_name: str) float[source]
Convert total kinetic energy (MeV) to energy per nucleon (MeV/u).