Projected Heat-source Calibration
calibrateHeatSource estimates the two coefficients that relate melt-pool depth to the axial shape of AdditiveFOAM’s projected heat sources. It runs a collection of AdditiveFOAM cases, builds a local AdditiveFOAM response curve for every experimental condition, infers one local shape parameter per condition, and then fits the global projected-source closure. The command also caches completed simulations and writes a PDF report, a machine-readable fit, and a CSV summary.
This workflow calibrates the common depth projection used by projectedGaussian, nLightAFX, and tabulated. It does not calibrate the lateral beam profile, source dimensions, absorptivity, material properties, or boundary conditions. Those inputs must be established before interpreting the fitted depth-distribution coefficients.
The heat-source model reference defines the complete normalized volumetric distribution. This section focuses on the inverse problem solved by the calibration command.
Model being calibrated
The projected models use the axial function
\[p(z;k,d_z)=\exp\left[-3\left(\frac{z}{d_z}\right)^k\right],\]with normalization
\[\mathcal A_p(k,d_z)=\int_0^\infty p(z;k,d_z)\,\mathrm dz =\frac{d_z\Gamma(1/k)}{k3^{1/k}}.\]Here \(z\) is distance into the material, \(d_z\) is the current source depth, and \(k\) controls the depth-profile shape. AdditiveFOAM computes \(k\) through
\[x_s=\max\left[\frac{d_z}{s_0},10^{-3}\right],\] \[n=\operatorname{clip}_{[0,9]}\!\left[A\log_2(x_s)+B\right], \qquad k=2^n,\]where
\[s_0=\min(d_x^0,d_y^0)\]is the smaller configured lateral source dimension. The superscript \(0\) identifies the dimensions read when the heat-source model is constructed; transient depth updates change \(d_z\) but not \(s_0\).
Increasing \(n\) increases \(k\), which makes the source more nearly uniform through most of its depth and sharpens its decay near \(d_z\). The clipping limits imply \(1\leq k\leq512\).
Calibration overview
The calculation has two stages:
- For each experimental process condition, set \(A=0\) and infer a local value \(n_c=B_c\) by comparing measured melt-pool depths with AdditiveFOAM trials.
- Regress the local estimates against normalized measured depth to obtain the production coefficients \(A\) and \(B\).
The index \(c\) denotes an experimental condition, such as one combination of power, scan speed, and spot size. The index \(r\) denotes repeated depth measurements at that condition, and \(j\) denotes a trial value used in an AdditiveFOAM response curve.
Experimental observations
Each entry in experiments.yml supplies a process-condition dictionary and one or more measured depths:
- parameters:
Power_W: 300.0
Speed_mm_s: 500
Spot_Size_microns: 109.69
Measured_Depth_microns: [168.57, 190.67]
Spot_Size_microns is the measured full D4σ beam diameter. For condition \(c\), denote it by \(D_c\). The command validates that it is finite and positive, converts it to the 2σ radius
and renders \(w_c\) in metres through the <<heatSourceRadius>> placeholder. The supplied template assigns it to both lateral entries in dimensions, ensuring that every trial uses the beam size associated with its experimental condition.
The statistical calibration normalizes measured depth \(d_{c,r}\) by that same lateral heat-source dimension:
\[y_{c,r}=\frac{d_{c,r}}{w_c} =\frac{d_{c,r}}{D_c/2}.\]Stage 1: local AdditiveFOAM calibration
Trial simulations
At condition \(c\), every AdditiveFOAM trial fixes \(A=0\) and sets the configured calibration token to a trial value \(b_j\). The projected-source relation therefore reduces to
\[n=b_j,\qquad k=2^{b_j}.\]The command copies the case template, replaces the placeholders for trial value, 2σ source radius, power, velocity, end time, and write interval, and runs the configured case command. Spot size is converted from micrometres to metres, and scan speed is converted from mm/s to m/s.
The meltPoolDimensions Function Object writes the isotherm history. The response recorded for trial \(j\) is the maximum penetration over the simulated track,
and its normalized value is
\[\widehat y_{c,j}=\frac{\widehat d_{c,j}}{w_c} =\frac{\widehat d_{c,j}}{D_c/2}.\]When case.melt_pool_isovalue is liquidus or solidus, foamDictionary expands constant/transportProperties, reads thermoPath, and selects the temperature paired with alpha.solid = 0 or alpha.solid = 1, respectively. A numeric temperature may be supplied instead.
Response surrogate
The sampled pairs \((b_j,\widehat y_{c,j})\) are sorted by \(b_j\). With at least three unique trials, the command evaluates a shape-preserving piecewise-cubic Hermite interpolant on a dense grid:
\[\widehat y_c(n)=\operatorname{PCHIP} \left(\{b_j,\widehat y_{c,j}\}\right).\]With only two unique trials, the sampled points are used directly and the likelihood interpolates linearly between them. The surrogate does not extrapolate beyond the smallest and largest completed trial values. PCHIP preserves local curve shape, but it does not force a non-monotonic AdditiveFOAM response to become monotonic.
Bayesian inverse problem
The local prior is uniform over the simulated range:
\[n_c\sim\mathcal U\!\left(b_{\min},b_{\max}\right).\]Repeated normalized depth measurements are represented with independent normal likelihoods:
\[y_{c,r}\mid n_c\sim \mathcal N\!\left(\widehat y_c(n_c),\sigma_c^2\right),\]where
\[\sigma_c=\max\left[ 0.05\,\overline y_c, \operatorname{std}\!\left(\{y_{c,r}\}\right) \right].\]The five-percent floor prevents identical or nearly identical repeated measurements from producing a zero-width likelihood. PyMC samples the posterior using the configured draws, tuning steps, cores, target acceptance, and random seed.
The reported local estimate is the retained posterior sample with the largest sampled log probability. The state file also records the mean, median, variance, standard deviation, 2.5th and 97.5th percentiles, and up to 1,000 posterior samples.
Optional response-curve refinement
After the first posterior is sampled, the driver may add an AdditiveFOAM point when all of the following remain true:
- Fewer than
max_simulations_per_experimenttrials exist. - The posterior standard deviation exceeds
posterior_std_tolerance. - No completed trial matches the mean measured depth within
depth_tolerance_microns. - The target depth crosses the PCHIP response curve within
calibration.bounds.
The new trial is obtained by interpolating the first response-curve crossing of the mean measured depth. The AdditiveFOAM case is run, the surrogate is rebuilt, and the posterior is sampled again. Refinement stops as soon as either uncertainty or depth error satisfies its tolerance, the trial limit is reached, or no in-range crossing exists.
If the initial-value list already contains max_simulations_per_experiment values, as in the supplied tutorial, the response curve is an exhaustive fixed grid and no additional trial can be added.
Stage 2: global closure fit
For every completed condition, the global fit uses the mean normalized measured depth
\[x_c=\frac{1}{N_c}\sum_{r=1}^{N_c}\frac{d_{c,r}}{w_c}, \qquad z_c=\log_2(x_c),\]the local posterior mode \(n_c\), and its posterior standard deviation \(s_{n,c}\). The standardized residual is
\[r_c(A,B)=\frac{n_c-(Az_c+B)}{s_{n,c}}.\]Weighted linear least squares supplies the initial estimate. With at least three conditions, SciPy then minimizes the robust objective
\[\frac12\sum_c\rho\!\left(r_c^2\right),\]using soft_l1 by default. The uncertainty weighting gives better-constrained local calibrations more influence, while the robust loss limits the influence of an isolated condition that is inconsistent with the global trend.
The approximate parameter covariance is
\[\operatorname{Cov}(A,B)\approx (J^\mathsf TJ)^{-1}\max(\chi_r^2,1),\]where \(J\) is the uncertainty-scaled design matrix and \(\chi_r^2\) is the reduced weighted residual sum of squares.
The reported production relation is
\[n(x)=\operatorname{clip}_{[0,9]}\!\left[A\log_2(x)+B\right], \qquad k(x)=2^{n(x)}.\]The robust fit itself is performed on the unclipped linear relation. Clipping is applied when the response curve is evaluated and when AdditiveFOAM uses the coefficients.
Empirical uncertainty band
The final response interval propagates the local posterior uncertainty through complete refits:
- Draw one \(n_c\) from each stored local posterior. If samples are unavailable, draw from a normal approximation using the stored local mode and standard deviation.
- Clip the sampled local values to \([0,9]\).
- Refit \(A\) and \(B\) with the same robust, uncertainty-weighted procedure.
- Evaluate and clip the response curve over the plotted \(x\) range.
The pointwise 2.5th, 50th, and 97.5th percentiles of the successful bootstrap curves form the empirical interval. The report also records corresponding percentile intervals for \(A\) and \(B\). If too few bootstrap fits succeed, the plot falls back to the covariance approximation.
Normalization contract
Spot_Size_microns is the measured full D4σ diameter. The driver computes the 2σ heat-source radius
renders it into both lateral source dimensions, and uses it in the calibration coordinate:
\[x=\frac{d_z}{w} =\frac{d_z}{D_{4\sigma}/2}.\]The projected heat-source models evaluate the same coordinate:
\[x=\frac{d_z}{\min(d_x^0,d_y^0)}.\]For the supplied circular source, \(d_x^0=d_y^0=w\), so the calibration and runtime relations are identical.
Use A and B from calibration_fit.yml directly as the source slope and source intercept.
Configuration
config.yml contains five functional groups:
| Section | Entry | Meaning |
|---|---|---|
paths |
experiments |
Experimental YAML file |
paths |
template |
Complete AdditiveFOAM case containing renderer placeholders |
paths |
campaign |
Directory for generated cases, caches, fit, and reports |
case |
command |
Command run inside each rendered case |
case |
keep_successful |
Retain or remove processor and numeric time directories after metrics are saved |
case |
melt_pool_isovalue |
liquidus, solidus, or a temperature in kelvin |
calibration |
token |
Placeholder name used for the local trial parameter |
calibration |
initial_values |
Initial local values used to form each response curve |
calibration |
bounds |
Permitted local-value interval |
calibration |
max_simulations_per_experiment |
Maximum AdditiveFOAM trials for one condition |
calibration |
depth_tolerance_microns |
Stop refinement when a trial is this close to mean measured depth |
calibration |
posterior_std_tolerance |
Stop refinement when local posterior uncertainty falls below this value |
calibration |
pchip_grid_points |
Dense-grid resolution for the response surrogate |
bayesian |
draws, tune, cores |
PyMC sampling controls |
bayesian |
target_accept |
Sampler target acceptance probability |
bayesian |
random_seed |
Reproducible local-posterior seed |
final_fit |
bootstrap_samples |
Number of uncertainty-propagation refits |
final_fit |
bootstrap_random_seed |
Reproducible bootstrap seed |
report |
enabled |
Generate the PDF and CSV report products |
Relative paths are resolved from the directory containing config.yml. Environment variables and ~ are expanded.
Case-template contract
The template must contain 0/, constant/heatSourceDict, constant/scanPath, constant/transportProperties, system/controlDict, and Allrun. The renderer requires these placeholders exactly once:
| File | Placeholder |
|---|---|
constant/heatSourceDict |
The token selected by calibration.token, such as <<B>>, and <<heatSourceRadius>> |
constant/scanPath |
<<power>> and <<velocity>> |
system/controlDict |
<<endTime>> and <<writeInterval>> |
The current end-time calculation sums the travel time of mode-0 linear segments. It does not add mode-1 dwell durations. Calibration templates should therefore use the supplied initial-position row followed by translating segments, unless endTime handling is extended for dwell-based paths.
Cache and output files
The campaign directory is persistent:
campaign/
├── cases/<condition>/<trial>/
│ ├── log.run
│ └── metrics.yml
├── simulations.yml
├── calibration_state.yml
├── calibration_fit.yml
└── reports/
├── calibration_report.pdf
└── calibration_summary.csv
simulations.yml stores completed trial values and simulated depths by process-condition dictionary. calibration_state.yml stores local posteriors; a condition is considered current when its parameter dictionary matches and the fingerprint of its measured-depth list is unchanged. calibration_fit.yml stores the final coefficients, diagnostics, covariance approximation, bootstrap intervals, and calibrated \(x\) range.
The cache fingerprint does not include the case template, material files, solver revision, mesh, calibration settings, or Bayesian settings. After changing any of those inputs, remove the affected cached simulation and state data, or choose a new campaign directory, before rerunning the calibration.
With keep_successful: false, rendered inputs, log.run, Function Object output, and metrics.yml remain, but processor and numeric time directories are removed after the maximum melt-pool depth is recorded.
The complete worked example is the heat-source calibration tutorial.