Browse documentation
aner.parallel · Reference

Independent simulation repetitions

Repeat checked local task functions from identical copied inputs, preserve deterministic iteration seeds and ordered numeric results, and inspect statistics, bounded storage and explicit CSV export.

parallel.repeat runs one checked, directly named Aner function repeatedly from the same initial parameters. Each iteration owns a private parameter copy and local state, receives its zero based identity and deterministic seed, and contributes one numeric output row. A bounded native CPU executor schedules the work; results retain iteration order even when workers finish in a different order.

This is a focused API guide within aner.parallel. Keep import aner.parallel; there is no separate repetition submodule to import. Use the root guide for CPU allocation, fixed Monte Carlo workloads and terminal jobs, or distributed neural training for the separate Dense replica contract.

Start with independent stochastic growth paths

Download the local repetition program, then check and run it from your checkout or binary bundle:

sh
aner check examples/parallel/repeat_local.aner
aner examples/parallel/repeat_local.aner

The program performs 1,000 independent paths of a synthetic multiplicative growth process. Its identical initial parameters are an initial level, per step drift, noise scale and a fixed number of steps. Each task maintains its own level and peak, and returns two dimensionless outputs: final relative change and largest relative decline from a previous peak. It requires no service, data download, credentials or accelerator and writes no file by default.

The fixture illustrates the execution API with simple model arithmetic. Researchers can replace the task body with their scientific model and explicitly validate its units, assumptions and numerical behavior. The returned statistics describe this configured synthetic process; they do not establish model validity.

Each step uses level = level × (1 + drift + noise_scale × shock), where shock is the seeded standard normal draw for that step. The initial level is an abstract quantity with no domain calibration. Column zero is final_level/initial_level − 1; column one is the largest (previous_peak − level)/previous_peak along the path. Both outputs are dimensionless.

The central call is:

aner
let initial_parameters = List(100000.0, 0.0002, 0.01, 64)
let results = parallel.repeat(iterations: 1000, task: simulate_growth_path,
    parameters: initial_parameters, execution: parallel.local(), seed: 42)
print(results.mean(column: 0))
print(results.std(column: 0, sample: true))

This fragment belongs to the complete downloaded source; simulate_growth_path must be declared there. The source uses automatic local allocation so it also runs on a single admitted CPU. When at least four CPU slots are allocated, request four explicitly:

aner
let results = parallel.repeat(iterations: 1000, task: simulate_growth_path,
    parameters: initial_parameters,
    execution: parallel.local(workers: 4), seed: 42)

A request above the discovered allocation fails rather than silently clamping the worker count. parallel.inventory() describes the local policy. Four workers request concurrency; they do not guarantee four times the speed.

The first two output lines are 1000 and 2. The later lines show measured column statistics, the first derived seed and a JSON execution summary. These values describe the configured fixture and native build; elapsed time varies by machine. The task's two numeric columns remain in the documented order.

Continue with a spatial research model

Heat diffusion with uncertain material and source parameters develops a complete one dimensional transient model. It runs 1,000 independent realizations from the same initial temperature field, advances a heterogeneous spatial grid, and returns peak, final center, final spatial mean and threshold outputs. Its guide states the equations, units, explicit stability bound and Monte Carlo interpretation. It uses the same checked task API and requires at least four admitted CPU slots.

Parallelize only the dice loop

Download the complete balanced and weighted dice example. It keeps setup and post analysis serial and places only the 1,000 paired rolls between BEGIN PARALLEL and END PARALLEL comments. These comments mark the example's execution boundary; the parallel.repeat call selects parallel execution.

sh
aner check examples/parallel/repeat_dice.aner
aner examples/parallel/repeat_dice.aner

This program requires the updated native engine and at least four admitted CPU slots. Checking starts no repetitions. Running needs no external data, network, credentials or service and creates no file by default.

  1. Serial setup defines balanced weights List(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) and unbalanced weights List(50.0, 10.0, 10.0, 10.0, 10.0, 10.0). Every iteration receives a private copy of these same two Lists.
  2. The checked roll_pair task uses cumulative relative weights to select faces 1 through 6. It requests parallel.random(seed: seed, draw: 0) for the balanced die and a separate draw: 1 for the unbalanced die, then returns one two value List. Workers do not append to a shared output List; the executor collects one row per iteration.
  3. After the call completes, serial code reads the two result columns, calculates means and builds an Array<Int64> face count histogram for each die. Each histogram sums to 1,000; there are 1,000 paired rows, rather than 1,000 rolls divided among the workers.

The complete source declares roll_pair and initial_parameters; its parallel section is:

aner
let results = parallel.repeat(iterations: 1000, task: roll_pair,
    parameters: initial_parameters,
    execution: parallel.local(workers: 4), seed: 42)

Results have 1,000 rows and two columns. Faces returned as Int64 are stored exactly as native Float64 values; results.column(column: 0) and results.column(column: 1) export the balanced and unbalanced rolls in iteration order. The theoretical means are 3.5 and 2.5 respectively, because the second die gives face 1 probability 0.5 and each other face probability 0.1. Sample averages and face counts vary with the seed. The probability distributions match the supplied weights. Aner’s seeded generator determines the reproducible sample sequence.

Only the paired roll task runs on workers. Setup, printing and histograms execute on the caller after all results are available. Two small weighted selections per task may be too cheap to benefit from four workers: private copying and scheduling can cost more than the useful work. Treat this example as a partial parallel execution pattern, then measure a representative workload before claiming a speedup.

Declare a checked task

A task is the direct, unquoted name of a top level function with exactly these input types:

aner
fn simulate(parameters: List, iteration: Int64, seed: Int64) -> Float64 {
    let center = cast<Float64>(parameters.get(0))
    let spread = cast<Float64>(parameters.get(1))
    return center + spread * parallel.normal(seed: seed, draw: 0)
}

A scalar task can return Float64 or Int64. A multivariate task returns a List containing only finite numeric values. Every iteration must return the same nonempty width. Int64 outputs must be within -9007199254740992 through 9007199254740992, the exact integer range admitted for Float64 storage. Larger Int64 outputs fail rather than silently rounding. Convert explicitly with Float64(...) inside the task only when that rounding is intentional.

ParallelTask appears in the signature reference as an internal argument contract. It is not a general source value, closure, String identifier or function pointer that can be stored in a variable. Write task: simulate, rather than task: "simulate", task: simulate(...) or a function alias. The native checker resolves the declared function and its allowed call graph before execution.

Pass scenario inputs through parameters. A task cannot capture a global variable, including an outer variable holding those inputs. Local variables and supported numeric/collection/class operations belong to the task's private execution. Supported nested mutable values are copied, with their relationships preserved inside that copy. Mutating one task's parameters does not mutate another task or the caller's originals. Each repetition begins again from the submitted initial state; worker reuse does not accumulate a previous repetition's state.

The initial snapshot is bounded to 16 MiB, including copied descriptors and payloads, in addition to the runtime's existing heap, collection, array and execution quotas. That budget is separate from result storage. The admitted task slice rejects print, I/O, network/database/API/credential operations, development/visual recording, nested parallel execution and neural/Tensor/Dataset/ML/data/context operations. The stateless parallel.random and parallel.normal helpers are explicitly admitted. Clock reads such as DateTime.now and Duration.monotonic are rejected; pass an intended time value explicitly instead. Checked named helper functions and supported local class methods follow the same effect and capture constraints. General shared closures, unrestricted callbacks and remote task execution remain outside this API.

Checking a complete source with aner check validates the task call and allowed source graph without running the repetitions. Runtime shape, finite value, copy, allocation and task execution failures still require execution to detect.

API reference

The first three arguments are required. Execution, master seed and result budget have defaults and may be supplied independently by name. Defaults are automatic local execution, master seed 42 and 67108864 bytes (64 MiB):

CallResultContract
parallel.repeat(iterations: Int64, task: ParallelTask, parameters: List)SimulationResultsDefault local allocation, master seed and result budget.
parallel.repeat(iterations: Int64, task: ParallelTask, parameters: List, execution: ParallelContext)SimulationResultsExplicit local CPU policy, default seed and result budget.
parallel.repeat(iterations: Int64, task: ParallelTask, parameters: List, execution: ParallelContext, seed: Int64)SimulationResultsExplicit local policy and master seed, default result budget.
parallel.repeat(iterations: Int64, task: ParallelTask, parameters: List, execution: ParallelContext, seed: Int64, max_bytes: Int64)SimulationResultsExplicit execution, master seed and result allocation budget.

The table shows the ordinary positional forms. Named settings can skip preceding defaults, for example:

aner
let results = parallel.repeat(iterations: 1000, task: simulate_growth_path,
    parameters: initial_parameters, seed: 42, max_bytes: 67108864)

No explicit context is needed in that call. Named labels do not make the first three arguments optional; unknown or duplicate labels remain errors. Cluster and device contexts are rejected by repeat; use parallel.local(...).

Results methodResult and contract
results.rows() -> Int64Number of completed repetitions.
results.cols() -> Int64Common numeric output width; a scalar task has one column.
results.value(iteration: Int64, column: Int64) -> Float64One stored value with zero based indices.
results.row(iteration: Int64) -> ListDetached numeric row copy.
results.column(column: Int64) -> ListDetached numeric column copy in iteration order.
results.mean() -> Float64Mean of column zero.
results.mean(column: Int64) -> Float64Mean of the selected numeric column.
results.variance() -> Float64Population variance of column zero.
results.variance(column: Int64) -> Float64Population variance of the selected column.
results.variance(column: Int64, sample: Bool) -> Float64Population variance when false; sample variance when true.
results.std() -> Float64Population standard deviation of column zero.
results.std(column: Int64) -> Float64Population standard deviation of the selected column.
results.std(column: Int64, sample: Bool) -> Float64Population or sample standard deviation.
results.seed() -> Int64Submitted master seed.
results.seed(iteration: Int64) -> Int64Derived positive task seed for the selected iteration.
results.workers() -> Int64Admitted local worker allocation, not live utilization.
results.seconds() -> Float64Measured native executor scheduling, private per iteration copying/evaluation and row commits; excludes initial admission/snapshot, context startup, final publication, statistics and CSV export.
results.summary() -> StringJSON shape, seed, allocation, timing and managed storage accounting.
results.save_csv(path: String) -> UnitWrite a new private CSV file; refuse an existing destination.

Module forms include the first result argument, for example parallel.mean(result: results, column: 0) and parallel.row(result: results, iteration: 0). Results share immutable retained storage when handles are copied. Exported Lists are independent values; changing one does not rewrite the result.

Population variance divides by n, and sample variance divides by n - 1. Sample variance and standard deviation require at least two repetitions. Named sample: true can omit the column and uses column zero: results.std(sample: true) or results.variance(sample: true). An explicit column remains available, such as results.std(column: 1, sample: true). A positional Bool alone is not a column argument; use the named sample setting. Mean uses bounded exponent binned compensated summation; variance and standard deviation use scale normalization with compensated/Welford accumulation in fixed row order. Mean scratch storage is a fixed set of 2,099 bins (tens of KiB, depending on native long double), rather than another iterations sized result buffer. A nonzero statistic outside finite Float64 representation fails explicitly. A standard deviation is computed without first requiring its squared variance to fit Float64.

Deterministic random draws

CallResult and contract
parallel.random(seed: Int64) -> Float64Uniform value in [0, 1) at draw zero.
parallel.random(seed: Int64, draw: Int64) -> Float64Uniform value at an explicit nonnegative draw identity.
parallel.normal(seed: Int64) -> Float64Standard normal variate at draw zero.
parallel.normal(seed: Int64, draw: Int64) -> Float64Standard normal variate at an explicit nonnegative draw identity.

Both seed and draw inputs must be nonnegative Int64 values; the default draw is zero. The repeat master seed also must be nonnegative. These helpers are stateless: the same seed and draw repeat the same value, and another task cannot consume their shared sequence. Inside a task, use its supplied seed and a deliberate draw identity such as the loop step. Using a constant draw in every loop iteration deliberately repeats that one shock. When different random decisions share a step, give each decision a distinct draw identity rather than accidentally reusing it.

Task seeds derive from the master seed and zero based iteration identity and are nonzero positive 63 bit integers. Worker assignment and completion order do not change them. The bounded seed space does not guarantee unique seeds for every possible master/iteration pair. These are simulation streams and are not suitable for credentials or cryptographic randomness.

normal uses native Box Muller arithmetic with mean zero and standard deviation one. Transcendental math libraries can differ across builds and platforms, so bitwise normal draw or simulation result equality across different libm implementations is not promised. Worker count comparisons use the same checked source, engine build, inputs and seed; numerical portability comparisons should state their tolerance and build provenance.

Run 10,000 repetitions per scenario

The complete scenario program runs two scenarios separately, each with 10,000 repetitions and one identical initial parameter set. It changes only the declared noise scale between scenarios:

aner
let execution = parallel.local()
let lower_noise = parallel.repeat(iterations: 10000, task: simulate_growth_path,
    parameters: List(100000.0, 0.0002, 0.01, 64), execution: execution, seed: 42)
let higher_noise = parallel.repeat(iterations: 10000, task: simulate_growth_path,
    parameters: List(100000.0, 0.0002, 0.025, 64), execution: execution, seed: 42)

The fixed master seed deliberately matches the stochastic paths across those two parameter comparisons. The supplied per iteration seed is the same for equal iteration identities; the resulting outputs can differ because the parameters differ. This is common random number experimental control, not extra independence between matched scenarios. Change the master seed when separate streams are intended. Different seeds select another stream; they do not prove statistical independence.

sh
aner check examples/parallel/repeat_scenarios.aner
aner examples/parallel/repeat_scenarios.aner

The source prints each scenario's row count, relative change statistics, seed equality check and execution summary. It is a larger CPU workload than the first example. Keep all 10,000 rows when analyzing the output distribution; a mean or standard deviation alone does not preserve the tails or per run identity.

Save outputs deliberately

After computing a result, select a new output file:

aner
results.save_csv("growth-results.csv")

The CSV header is iteration,seed,value_0,value_1 for the two column growth fixture. Rows follow iteration order. Seeds are exact decimal integers; numeric outputs use locale independent Float64 max_digits10 precision for round trip reading. Column meanings still belong in the scenario's model documentation; this API does not infer names or units from a returned List.

The parent directory must already exist. Creation is exclusive, so an existing file, directory or leaf symlink fails. POSIX output files are owned by the current user with mode 0600; Windows uses a protected owner only ACL in the native implementation. Choose a new name for another export. The downloaded examples keep CSV writes commented so rerunning them does not fail because of an earlier output file.

This export stores completed output, not a durable simulation job checkpoint. A failed or interrupted repetition batch does not publish a partial SimulationResults. Recovery, source level detached repetition jobs and remote callback transport are not implemented.

Limits, errors and observation

Admission limitBehavior
1 through 1,000,000 iterationsEvery admitted row must complete before a result is published.
1 through 4,096 numeric columnsThe first completed valid row establishes width; every row must match it.
max_bytes from 1 through 1,073,741,824Default 64 MiB. Counts allocated dense Float64 capacity plus per iteration completion flags.
Initial parameter snapshot at most 16 MiBSeparate from output allocation; existing heap/array/collection quotas and task execution limits still apply.
Local CPU worker allocationUses the existing bounded parallel.local policy. Explicit requests exceeding the discovered allocation fail.
Finite Float64 outputs and statisticsNonnumeric, empty, changing width or nonfinite outputs fail visibly. Unrepresentable nonzero statistics fail.

For n iterations and c columns, the dense numeric payload is 8 × n × c bytes. Completion flags also need admission space; allocator capacity can exceed the logical payload. summary() distinguishes retained result_bytes from the peak managed allocation managed_bytes. Fixed metadata, allocator overhead, copied task heaps, interpreter state and executor workspace are outside max_bytes; it is not a total process memory quota.

Runtime repetition, RNG, accessor, statistical and CSV failures use source located R2803. Invalid checked task signatures, forbidden effects and global captures use E2803. Invalid task shape, capture or effect use is rejected before execution when statically knowable. A task exception stops publication and is reported as failure; the API never treats missing, duplicate or invalid rows as successful zeros. Bounds failures do not silently select another row or column. Row/column List exports also respect the caller's existing 262,144 field aggregate heap limit; keep a large result in its native representation or export CSV when a complete List would exceed that budget. CLI interruption and native executor cancellation stop the run rather than returning a partial success.

Development and production keep the same tasks, parameters, seed derivation and result ordering. Development may inspect outputs after the call; tasks cannot emit their own Dev captures or print from worker execution. Production disables development recording without changing the admitted task or scientific computation. A notebook cell waits until repetition completes; live worker timelines and streaming observations are not part of this slice.

Time, space and performance

Let n be repetitions, c returned columns, p admitted workers, h initial copied parameter storage, and T_i/W each task's work/working storage. The native executor provides bounded local CPU concurrency; the current engine still interprets checked Aner task source in its native runtime. This API does not compile arbitrary task functions ahead of time or turn an ordinary source loop into a vectorized GPU kernel.

OperationTimeAdditional space
Repeat all tasks and collect rowsO(sum of T_i + n h + n c)O(n c + p (h + W)), plus bounded scheduling/interpreter state
Read shape, allocation, time, seed or one stored valueO(1)O(1)
Copy one rowO(c)O(c) returned List
Copy one columnO(n)O(n) returned List
Mean, variance or standard deviation of one columnO(n)O(1) auxiliary numeric state
Export CSVO(n c) numeric formatting plus bytes writtenO(c) row formatting, excluding retained results

Recorded optimized CPU comparison

On October 14, 2026 UTC, an optimized native engine was measured on Atlas-01 / C04, an Apple M2 Max with 12 CPU cores, using the checked benchmark source. The final updated engine was verified separately as Atlas-01 / C05; the new binary fingerprint creates a new configuration revision on the same platform. Each run evaluated 10,000 paths × 200 scalar steps, with initial parameters List(100.0, 200, 0.01, 0.2), master seed 2026, and two outputs (final value and peak value). The same engine, source task, parameters and seeds were used for one versus four local workers. All output cells and derived per iteration seeds matched exactly in every trial. Trial 2 ran the four worker case first to vary ordering.

Configuration and trialOne worker executor secondsFour worker executor secondsMeasured ratioWhole source wall seconds
Atlas-01 / C04, trial 10.9060590.2342703.87×1.159093
Atlas-01 / C04, trial 2, reverse order0.9142390.2340323.91×1.176138
Atlas-01 / C04, trial 30.9138870.2307833.96×1.162710
Atlas-01 / C05, final quiet verification0.8988692080.2359013753.81036×1.153540042

The C05 verification ran after other development checks and local source packaging completed, with ordinary operating system/background activity outside the experiment's control. It again checked every output cell and derived iteration seed and retained exact parity between one and four workers. C04's three measured trials remain their own record; the one final C05 verification is not a three trial estimate.

Reproduction identities are SHA-256 content fingerprints, not digital signatures:

MaterialSHA-256
Checked benchmark source, C04 and C056abea8f562a7e897278e32c0f79d29736071cb9602cc7c9b7a8f66bb8ab5a181
Native executable, Atlas-01 / C049e045c106cbbb822e0df5ba0ce9528016203b92b3653045f6fc20b5051d6fe80
Native executable, Atlas-01 / C05dfd628c32ee7a9c3feed1fb1a4498da5b9c66869daa52d420d33574f6d6c4f21

Both native configurations use the receipt recorded Release build with Apple clang 21.0.0, C++20, -O3, -DNDEBUG, -fno-fast-math and -ffp-contract=off. The verified machine record is arm64 macOS 26.7.1 (build 25G241), 12 logical/physical CPU cores and 64 GiB installed RAM. Capacity is not measured process memory or proof of dedicated CPU utilization. An engine or hardware change requires another immutable configuration revision.

Executor timings use the results.seconds() boundary above. Each whole source wall measurement includes both worker configurations, source checking, row/seed equality checks and statistics; it is not an individual four worker latency. This is a measured synthetic same engine scaling comparison. The timing applies to the recorded workload and allocation; it does not establish scientific model validity or guarantee a universal four worker speedup.

Compare one and four workers with the same task, parameters, repetition count and master seed. Verify stored rows and seeds before comparing elapsed time, then report multiple measured trials on the admitted CPU allocation. Initial admission/snapshot, context/thread startup and final publication are outside results.seconds() and belong in a separate end to end wall time measurement. Task cloning, short tasks, result copies, task arithmetic and resource contention can dominate. More workers may be slower for small workloads. No universal speedup or task completion estimate is implied.

The reproducibility benchmark program compares 10,000 repetitions of the same task on one and four admitted CPU workers, checks all seeds/output rows, and prints measured timing. It requires at least four allocated CPU slots and is a larger CPU workload; measured results belong with their build, task and hardware record.

Continue with the aner.parallel root guide, development observations, and the complete local source.

Aner documentation · Reference and guides