Measured heat diffusion CPU scaling
Compare 10000 identical heat diffusion realizations on one and four local workers, verify every output and derived seed, and inspect actual executor timing.
// Heat-diffusion benchmark: 10000 realizations on one and four CPU workers.
// Same task, configuration and seed as repeat_heat_diffusion.aner.
// Requires four admitted CPU slots. Run with --max-steps 5000000.
// The two ensembles run in sequence; this benchmark creates no file.
// Set four_first to true to reverse the run order without changing the model.
import aner.parallel
class HeatExperiment {
public let points: Int64
public let length_m: Float64
public let dt_s: Float64
public let steps: Int64
public let pulse_steps: Int64
public let ambient_k: Float64
public let diffusivity_m2_s: Float64
public let diffusivity_spread: Float64
public let source_k_s: Float64
public let source_spread: Float64
public let threshold_k: Float64
}
fn finite_value(value: Float64) -> Bool {
return value - value == 0.0
}
fn validate_experiment(c: HeatExperiment) -> Result<HeatExperiment, String> {
if !finite_value(c.length_m) || !finite_value(c.dt_s) || !finite_value(c.ambient_k) ||
!finite_value(c.diffusivity_m2_s) || !finite_value(c.diffusivity_spread) ||
!finite_value(c.source_k_s) || !finite_value(c.source_spread) || !finite_value(c.threshold_k) {
return Result.err<HeatExperiment, String>("Every Float64 configuration field must be finite.")
}
if c.points < 3 || c.points % 2 == 0 {
return Result.err<HeatExperiment, String>("Use an odd grid of at least three points.")
}
if c.length_m <= 0.0 || c.dt_s <= 0.0 || c.steps < 1 {
return Result.err<HeatExperiment, String>("Length, time step and step count must be positive.")
}
if c.pulse_steps < 0 || c.pulse_steps > c.steps || c.ambient_k <= 0.0 {
return Result.err<HeatExperiment, String>("Pulse duration must fit the horizon; initial Kelvin temperature must be positive.")
}
if c.diffusivity_m2_s <= 0.0 || c.diffusivity_spread < 0.0 || c.diffusivity_spread >= 1.0 {
return Result.err<HeatExperiment, String>("Diffusivity must be positive with relative spread in [0, 1).")
}
if c.source_k_s < 0.0 || c.source_spread < 0.0 || c.source_spread > 1.0 || c.threshold_k <= 0.0 {
return Result.err<HeatExperiment, String>("Use nonnegative heating, source spread in [0, 1], and a positive Kelvin threshold.")
}
let dx = c.length_m / Float64(c.points - 1)
let dx2 = dx * dx
let alpha_max = c.diffusivity_m2_s * (1.0 + c.diffusivity_spread)
if !finite_value(dx2) || dx2 <= 0.0 || !finite_value(alpha_max) || alpha_max <= 0.0 {
return Result.err<HeatExperiment, String>("Grid spacing and maximum diffusivity must be positive and representable.")
}
let dt_over_dx2 = c.dt_s / dx2
let courant_sum = 2.0 * alpha_max * dt_over_dx2
let maximum_heating_increment = c.dt_s * c.source_k_s * (1.0 + c.source_spread)
if !finite_value(dt_over_dx2) || dt_over_dx2 <= 0.0 || !finite_value(courant_sum) ||
!finite_value(maximum_heating_increment) || courant_sum > 1.0 {
return Result.err<HeatExperiment, String>("Explicit diffusion parameters must be representable and stable: reduce dt_s or use a coarser grid.")
}
return Result.ok<HeatExperiment, String>(c)
}
fn heat_realization(parameters: List, iteration: Int64, seed: Int64) -> List {
let c = validate_experiment(cast<HeatExperiment>(parameters.get(0))).unwrap()
let dx = c.length_m / Float64(c.points - 1)
let dt_over_dx2 = c.dt_s / (dx * dx)
let alpha = Array<Float64>(c.points - 1, 0.0)
let source_shape = Array<Float64>(c.points, 0.0)
var temperature = Array<Float64>(c.points, c.ambient_k)
var next_temperature = Array<Float64>(c.points, c.ambient_k)
// Each face coefficient is drawn once, then shared by both adjacent fluxes.
var face = 0
while face < c.points - 1 {
let perturbation = 2.0 * parallel.random(seed, draw: face) - 1.0
alpha.set(face, c.diffusivity_m2_s * (1.0 + c.diffusivity_spread * perturbation))
face = face + 1
}
// A distinct draw controls one heating amplitude for this realization.
let amplitude_draw = parallel.random(seed, draw: c.points - 1)
let amplitude = 1.0 + c.source_spread * (2.0 * amplitude_draw - 1.0)
// Average the central-third heater over each node-centered control volume.
// This gives half strength at nodes 10 and 20 on the default 31-point grid.
var node = 1
while node < c.points - 1 {
let x = Float64(node) * dx
var left = x - 0.5 * dx
var right = x + 0.5 * dx
if left < c.length_m / 3.0 { left = c.length_m / 3.0 }
if right > 2.0 * c.length_m / 3.0 { right = 2.0 * c.length_m / 3.0 }
if right > left { source_shape.set(node, (right - left) / dx) }
node = node + 1
}
var peak_k = c.ambient_k
var step = 0
while step < c.steps {
var heating_increment = 0.0
if step < c.pulse_steps { heating_increment = c.dt_s * c.source_k_s * amplitude }
node = 1
while node < c.points - 1 {
let current = temperature.get(node)
let left_flux = alpha.get(node - 1) * (temperature.get(node - 1) - current)
let right_flux = alpha.get(node) * (temperature.get(node + 1) - current)
let updated = current + dt_over_dx2 * (left_flux + right_flux) +
heating_increment * source_shape.get(node)
next_temperature.set(node, updated)
if updated > peak_k { peak_k = updated }
node = node + 1
}
// Both arrays retain the fixed endpoint temperatures. Swap, do not update in place.
let previous = temperature
temperature = next_temperature
next_temperature = previous
step = step + 1
}
let center_k = temperature.get(c.points / 2)
let spatial_mean_k = (temperature.sum() - c.ambient_k) / Float64(c.points - 1)
var exceeded = 0.0
if peak_k > c.threshold_k { exceeded = 1.0 }
return List(peak_k, center_k, spatial_mean_k, exceeded)
}
// 1. SERIAL SETUP: one declared experiment; all initial/boundary nodes are 293.15 K.
let experiment = HeatExperiment(points: 31, length_m: 0.1, dt_s: 0.1,
steps: 400, pulse_steps: 200, ambient_k: 293.15,
diffusivity_m2_s: 0.00001, diffusivity_spread: 0.25,
source_k_s: 3.0, source_spread: 0.2, threshold_k: 335.0)
let checked = validate_experiment(experiment)
if checked.is_err() { print(checked.unwrap_err()) }
let initial_parameters = List(checked.unwrap())
// Each run evolves 31 points through 400 dependent time steps per realization.
// Initial parameters and physical assumptions are the same for both ensembles.
let four_first = false
var first_workers = 1
var second_workers = 4
if four_first { first_workers = 4; second_workers = 1 }
let first_execution = parallel.local(workers: first_workers)
let second_execution = parallel.local(workers: second_workers)
print("Starting first ensemble with workers: " + String(first_workers))
let first = parallel.repeat(iterations: 10000, task: heat_realization,
parameters: initial_parameters, execution: first_execution, seed: 2026)
print("First ensemble executor seconds: " + String(first.seconds()))
print("Starting second ensemble with workers: " + String(second_workers))
let second = parallel.repeat(iterations: 10000, task: heat_realization,
parameters: initial_parameters, execution: second_execution, seed: 2026)
print("Second ensemble executor seconds: " + String(second.seconds()))
var one_seconds = first.seconds()
var four_seconds = second.seconds()
if four_first { one_seconds = second.seconds(); four_seconds = first.seconds() }
print("One-worker executor seconds: " + String(one_seconds))
print("Four-worker executor seconds: " + String(four_seconds))
// Serial verification checks shapes, every ordered cell and every derived seed.
var identical = first.rows() == 10000 && second.rows() == 10000 &&
first.cols() == 4 && second.cols() == 4 && first.workers() == first_workers && second.workers() == second_workers
var mismatched_cells = 0
var mismatched_seeds = 0
var iteration = 0
while iteration < first.rows() {
if first.seed(iteration: iteration) != second.seed(iteration: iteration) {
mismatched_seeds = mismatched_seeds + 1
}
var column = 0
while column < first.cols() {
if first.value(iteration: iteration, column: column) !=
second.value(iteration: iteration, column: column) {
mismatched_cells = mismatched_cells + 1
}
column = column + 1
}
iteration = iteration + 1
}
identical = identical && mismatched_cells == 0 && mismatched_seeds == 0
var identical_statistics = true
var column = 0
while column < first.cols() {
if first.mean(column: column) != second.mean(column: column) ||
first.variance(column: column, sample: true) != second.variance(column: column, sample: true) ||
first.std(column: column, sample: true) != second.std(column: column, sample: true) {
identical_statistics = false
}
column = column + 1
}
print("Identical output cells and seeds: " + String(identical))
print("Identical means, sample variances and standard deviations: " + String(identical_statistics))
print("Output cells checked: " + String(first.rows() * first.cols()))
print("Derived seeds checked: " + String(first.rows()))
print("Mismatched output cells: " + String(mismatched_cells))
print("Mismatched derived seeds: " + String(mismatched_seeds))
print("Measured executor speedup: " + String(one_seconds / four_seconds))
if four_first {
print("One-worker summary: " + second.summary())
print("Four-worker summary: " + first.summary())
} else {
print("One-worker summary: " + first.summary())
print("Four-worker summary: " + second.summary())
}
column = 0
while column < second.cols() {
print("Column " + String(column) + " mean: " + String(second.mean(column: column)))
print("Column " + String(column) + " sample variance: " + String(second.variance(column: column, sample: true)))
print("Column " + String(column) + " sample std: " + String(second.std(column: column, sample: true)))
column = column + 1
}
print("Peak temperature mean (K): " + String(second.mean(column: 0)))
print("Peak temperature sample std (K): " + String(second.std(column: 0, sample: true)))
print("Final center temperature mean (K): " + String(second.mean(column: 1)))
print("Final spatial mean temperature (K): " + String(second.mean(column: 2)))
print("Fraction of peaks above 335 K: " + String(second.mean(column: 3)))
Make it your experiment.
With Aner installed, save this program as examples/parallel/repeat_heat_benchmark.aner inside a folder for your experiment. Open a terminal in that folder, then check and run the program.
aner check examples/parallel/repeat_heat_benchmark.aner
aner run examples/parallel/repeat_heat_benchmark.aner --max-steps 5000000The aner command must be on your PATH. Follow the installation guide if your terminal cannot find it.
Use the updated optimized native Aner executable with checked parallel.repeat tasks and SimulationResults. VS Code extension 0.1.39 supplies compatible argument and result help. This substantial CPU workload runs two 10000 realization ensembles sequentially, with 31 grid points and 400 time steps per realization, and requires at least four admitted CPU slots. The published command sets --max-steps 5000000 as an explicit interpreter budget. Checking performs no realizations; running compares every output cell, every derived seed and all four columns of statistics. No external data, network, service or credential is needed, and the benchmark creates no file.
Bundled datasets retain their own source attribution and license notices. Example outcomes are not comparative benchmarks or evidence of clinical validity.
Dataset sources & attribution