anapao 0.1.1

Library for deterministic simulation tests and reproducible stochastic workflows
Documentation

anapao

Crates.io Version CI CodSpeed Badge Crates.io Downloads License Discord Buymecoffee

anapao is a library-only deterministic Rust testing utility for simulation and stochastic workflows. It is intended to be used from Rust tests and tooling through the crate API, not as a command-line program. This README is a linear tutorial for new users: you will build one scenario, run it deterministically, add expectations, run Monte Carlo batches, and persist CI-friendly artifacts.

The README and generated crate documentation are self-contained public documentation. Any ignored local docs/ directory is reserved for private research notes and is not tracked, packaged, shipped, or required to use the crate.

What You Will Build

By the end, you will have a repeatable testing flow that can:

  • compile a ScenarioSpec into a validated executable model,
  • execute seeded deterministic single runs,
  • execute deterministic Monte Carlo batches,
  • evaluate typed assertions with evidence,
  • persist artifact packs (manifest.json, events.jsonl, series.csv, and more).

Prerequisites

  • Rust 1.85+
  • Cargo
  • A Rust test project where you want deterministic simulation checks

Add the library dependency:

[dependencies]
anapao = "0.1.0"

The crate does not install or expose a binary target; import anapao from your Rust code.


Step 1: Create ScenarioSpec

ScenarioSpec is your declarative model: nodes, edges, end conditions, and tracked metrics.

Snippet S01 — Build a Minimal Scenario

use anapao::types::{EndConditionSpec, MetricKey, ScenarioSpec, TransferSpec};

let mut scenario = ScenarioSpec::source_sink(TransferSpec::Fixed { amount: 1.0 })
    .with_end_condition(EndConditionSpec::MaxSteps { steps: 3 });
scenario.tracked_metrics.insert(MetricKey::fixture("sink"));

assert_eq!(scenario.nodes.len(), 2);
assert_eq!(scenario.edges.len(), 1);

What you learned:

  • how to bootstrap a minimum source->sink scenario with a convenience constructor,
  • how end conditions and tracked metrics are attached.

Step 2: Compile with Simulator::compile

Compilation validates and transforms your scenario into deterministic execution indexes.

Snippet S02 — Compile a Scenario

use anapao::types::{EndConditionSpec, ScenarioSpec, TransferSpec};
use anapao::Simulator;

let scenario = ScenarioSpec::source_sink(TransferSpec::Fixed { amount: 1.0 })
    .with_end_condition(EndConditionSpec::MaxSteps { steps: 3 });

let compiled = Simulator::compile(scenario).unwrap();
assert_eq!(compiled.scenario.id.as_str(), "scenario-source-sink");

What you learned:

  • compilation is explicit and deterministic,
  • you should compile once and reuse the compiled form for runs.

Step 3: Configure RunConfig

RunConfig controls deterministic single-run execution (seed, max_steps, capture policy).

Snippet S03 — Create a Deterministic RunConfig

use anapao::types::{CaptureConfig, RunConfig};

let run = RunConfig::for_seed(42).with_max_steps(250).with_capture(CaptureConfig {
    every_n_steps: 5,
    include_step_zero: true,
    include_final_state: true,
    ..CaptureConfig::default()
});

assert_eq!(run.seed, 42);
assert_eq!(run.max_steps, 250);
assert_eq!(run.capture.every_n_steps, 5);

What you learned:

  • seeds pin determinism,
  • capture configuration controls trace granularity.

Step 4: Execute a Deterministic Single Run

Now run one deterministic simulation and assert expected outputs.

Snippet S04 — Run Once and Verify Outputs

use anapao::{testkit, Simulator};
use anapao::types::MetricKey;

let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
let report = Simulator::run(&compiled, &testkit::deterministic_run_config()).unwrap();

assert!(report.completed);
assert_eq!(report.steps_executed, 3);
assert_eq!(report.final_metrics.get(&MetricKey::fixture("sink")), Some(&3.0));

What you learned:

  • deterministic single-run output can be asserted directly in tests.

Step 5: Create an Expectation Set

Expectation provides typed assertion semantics for run and batch reports.

Snippet S05 — Declare Expectations

use anapao::assertions::{Expectation, MetricSelector};
use anapao::types::MetricKey;

let metric = MetricKey::fixture("sink");
let expectations = vec![
    Expectation::Equals {
        metric: metric.clone(),
        selector: MetricSelector::Final,
        expected: 3.0,
    },
    Expectation::Approx {
        metric: metric.clone(),
        selector: MetricSelector::Final,
        expected: 3.0,
        abs_tol: 0.0001,
        rel_tol: 0.0,
    },
    Expectation::Between {
        metric,
        selector: MetricSelector::Final,
        min: 0.0,
        max: 10.0,
    },
];

assert_eq!(expectations.len(), 3);

What you learned:

  • expectations are data, not ad-hoc assertion code,
  • selector controls whether you validate final value vs specific step.

Step 6: Run with Assertions and Event Sink

Use the integrated assertion path and capture ordered events for diagnostics.

Snippet S06 — run_with_assertions_and_sink + VecEventSink

use anapao::assertions::{Expectation, MetricSelector};
use anapao::events::VecEventSink;
use anapao::types::MetricKey;
use anapao::{testkit, Simulator};

let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
let expectations = vec![Expectation::Equals {
    metric: MetricKey::fixture("sink"),
    selector: MetricSelector::Final,
    expected: 3.0,
}];

let mut sink = VecEventSink::new();
let (_report, assertion_report) = Simulator::run_with_assertions_and_sink(
    &compiled,
    &testkit::deterministic_run_config(),
    &expectations,
    &mut sink,
)
.unwrap();

assert!(assertion_report.is_success());
assert!(sink
    .events()
    .iter()
    .any(|event| event.event_name() == "assertion_checkpoint"));

What you learned:

  • assertions and execution can be done in one call,
  • event streams provide structured debugging context.

Step 7: Configure BatchConfig

BatchConfig controls deterministic Monte Carlo execution.

Snippet S07 — Create BatchConfig

use anapao::types::{BatchConfig, BatchRunTemplate, ExecutionMode};

let batch = BatchConfig::for_runs(64)
    .with_execution_mode(ExecutionMode::SingleThread)
    .with_base_seed(7)
    .with_run_template(BatchRunTemplate::default())
    .with_max_steps(50);

assert_eq!(batch.runs, 64);
assert_eq!(batch.base_seed, 7);
assert_eq!(batch.run_template.max_steps, 50);

What you learned:

  • runs scales the Monte Carlo sample size,
  • base_seed + run index derivation preserve reproducibility.

Step 8: Execute a Deterministic Batch Run

Run many deterministic simulations and check aggregate outputs.

Snippet S08 — Run Batch and Verify Ordering/Aggregates

use anapao::{testkit, Simulator};
use anapao::types::MetricKey;

let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
let batch = Simulator::run_batch(&compiled, &testkit::deterministic_batch_config()).unwrap();

assert_eq!(batch.completed_runs, batch.requested_runs);
assert!(batch.runs.windows(2).all(|window| window[0].run_index < window[1].run_index));
assert!(batch.aggregate_series.contains_key(&MetricKey::fixture("sink")));

What you learned:

  • batch summaries are deterministic and index-ordered.
  • completed_runs counts reported run summaries; inspect each run.completed for semantic completion.

Step 9: Persist Artifacts and Inspect ManifestRef

Persist reports for CI diffing and post-run diagnostics.

Snippet S09 — Full Playbook (Setup -> Run -> Assert -> Artifacts)

use anapao::artifact::write_run_artifacts_with_assertions;
use anapao::assertions::{Expectation, MetricSelector};
use anapao::events::VecEventSink;
use anapao::types::MetricKey;
use anapao::{testkit, Simulator};

let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
let expectations = vec![Expectation::Equals {
    metric: MetricKey::fixture("sink"),
    selector: MetricSelector::Final,
    expected: 3.0,
}];

let mut sink = VecEventSink::new();
let (run_report, assertion_report) = Simulator::run_with_assertions_and_sink(
    &compiled,
    &testkit::deterministic_run_config(),
    &expectations,
    &mut sink,
)
.unwrap();
assert!(run_report.completed);
assert!(assertion_report.is_success());

let output_dir = std::env::temp_dir().join("anapao-readme-playbook");
let manifest = write_run_artifacts_with_assertions(
    &output_dir,
    &run_report,
    sink.events(),
    Some(&assertion_report),
)
.unwrap();

assert!(manifest.artifacts.contains_key("manifest"));
assert!(manifest.artifacts.contains_key("events"));
assert!(manifest.artifacts.contains_key("assertions"));

What you learned:

  • persisted artifacts become your CI and debugging contract,
  • manifest keys are stable assertions for artifact expectations.

Step 10: Fixture-First Testing with testkit (and rstest)

Use testkit helpers to avoid duplicating setup across tests.

Snippet S10 — Reusable Fixture-Style Test Pattern

use anapao::{testkit, Simulator};
use anapao::types::MetricKey;

fn deterministic_fixture_smoke() {
    let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
    let report = Simulator::run(&compiled, &testkit::deterministic_run_config()).unwrap();
    assert_eq!(report.final_metrics.get(&MetricKey::fixture("sink")), Some(&3.0));
}

deterministic_fixture_smoke();

What you learned:

  • fixture helpers keep tests concise and deterministic,
  • you can wrap these helpers in your own rstest fixture macros for larger matrices.

Common Failure Modes and Debugging Hints

  • Missing tracked metric:
    • symptom: expectation fails with missing observed value.
    • fix: ensure metric key is in scenario.tracked_metrics.
  • Non-terminating scenarios:
    • symptom: run ends at max_steps unexpectedly.
    • fix: verify end_conditions are configured and reachable.
  • Seed confusion:
    • symptom: output differs between runs.
    • fix: pin RunConfig.seed for single runs and keep batch base_seed stable (batch seeds derive from base_seed + run index).
  • Sparse traces:
    • symptom: insufficient snapshots for diagnostics.
    • fix: adjust RunConfig.capture (every_n_steps, step-zero/final flags).

Feature Flags

  • parallel: enables Rayon-backed batch execution mode (ExecutionMode::Rayon).
  • analysis-polars: enables Polars DataFrame shaping helpers.
  • assertions-extended: enables extra assertion/snapshot/property helper crates.

CI intentionally validates a targeted feature surface instead of an exhaustive feature combination matrix. The supported check surface is the default feature set, each individual optional feature (parallel, analysis-polars, and assertions-extended), and the combined --all-features build.

Module Surface (Reference)

anapao exports:

  • types
  • error
  • rng
  • validation
  • engine
  • stochastic
  • events
  • batch
  • stats
  • artifact
  • assertions
  • testkit
  • analysis (only with analysis-polars)
  • Simulator (compile/run/batch facade)

Validation Commands

cargo test --doc
cargo test --all-targets
cargo test --all-targets --features parallel
cargo test --all-targets --features analysis-polars
cargo test --all-targets --features assertions-extended
cargo test --all-targets --all-features
cargo audit --deny warnings
cargo bench --no-run

Performance Workflow (Manual Compare)

# capture baseline matrix
./scripts/bench-criterion save --bench simulation --baseline hotspots-20260224-default
./scripts/bench-criterion save --bench simulation --features parallel --baseline hotspots-20260224-parallel

# compare matrix
./scripts/bench-criterion compare --bench simulation --baseline hotspots-20260224-default
./scripts/bench-criterion compare --bench simulation --features parallel --baseline hotspots-20260224-parallel

# manual non-failing regression summary (+7% threshold)
./scripts/bench-criterion summary --bench simulation --baseline hotspots-20260224-default --threshold 0.07
./scripts/bench-criterion summary --bench simulation --features parallel --baseline hotspots-20260224-parallel --threshold 0.07

# flamegraphs and csv summaries
./benchmarks/run_profiles.sh
BENCH_FEATURES=parallel ./benchmarks/run_profiles.sh

Dependency and Security Maintenance

CI runs cargo audit --deny warnings on every push and pull request to report RustSec advisories and dependency problems from Cargo.lock. Treat a failing audit as a release blocker unless the advisory is not reachable for this crate; if an advisory is not actionable immediately, document the reason and the planned follow-up in the pull request.

When updating dependencies:

  1. Prefer the smallest compatible version bump that resolves the advisory or maintenance need.
  2. Review changelogs for public API, MSRV, feature, and license changes before merging.
  3. Keep optional feature dependencies (parallel, analysis-polars, and assertions-extended) checked with the normal CI matrix instead of adding one-off release automation.
  4. Regenerate and commit Cargo.lock, then run cargo audit --deny warnings plus the standard repository validation commands.

Local Pre-commit

This repo ships a native prek.toml for fast local commit gates.

prek validate-config
prek run --all-files
prek install

The hooks intentionally stay lightweight: cargo fmt --all -- --check and cargo clippy --all-targets --all-features -- -D warnings.