Anulum Overview Host analysis AMP simulation Validation Python API Rust API C/C++ API

Host analysis contract

tools/analyze_run.py accepts one versioned run manifest and writes a report directory. The event-capture RTL and its testbench produce binary input in simulation. No board measurement or instrument acceptance has been performed; a simulation report has evidence_status: simulation_only and cannot be cited as a PolarFire SoC result.

.venv/bin/python tools/analyze_run.py path/to/manifest.json --output-dir path/to/new-report

The Python distribution is named loop-timing-witness and requires Python 3.13 or later. Its installed command runs the same analysis implementation:

loop-timing-witness-analyze path/to/manifest.json --output-dir path/to/new-report

The public Python API loads and verifies the manifest before calculating a report:

from pathlib import Path

from loop_timing_witness import build_report, load_run

inputs = load_run(Path("path/to/manifest.json"))
report, cycle_rows = build_report(inputs)

The wheel includes the analysis dependency closure, JSON contracts and py.typed marker. It does not require a source checkout to analyse a retained capture. Schemas shipped inside the package are checked against the repository contracts; each run still supplies its own hash-bound measurement-domain snapshot. Firmware preparation, capture execution and vendor-tool projects remain repository build surfaces. Local distribution verification does not establish publication on PyPI or qualify a board instrument.

Report-directory publication in the unreleased source and its locally built Python distributions is supported on Linux with an available renameat2(RENAME_NOREPLACE) native binding, kernel and target filesystem. The writer resolves this binding lazily and exercises it in a disposable sibling capability directory before staging report artefacts. An unavailable binding or failed capability probe refuses publication; there is no check-then-rename fallback. Other operating systems require explicit activation and equivalent native publication tests before support is claimed. The pure load_run and build_report APIs do not use this publication binding; the package and its portable wheel tag do not imply a report-writing guarantee on another OS.

The output path must not exist, including as a dangling symlink. The tool validates the JSON and every referenced SHA-256, decodes the binary events, analyses the series, writes all outputs to a sibling temporary directory and atomically publishes it only after every write succeeds. The native no-replacement operation refuses a destination created during staging, preserving the competing path and removing the staged report. An invalid run can still produce a report: valid: false and invalid_reasons retain the reason. Malformed inputs fail without a report. The source and hardware artefacts are checked as files, not accepted merely as hash strings. The tool accepts relative paths within the run directory only.

Inputs

run-manifest.schema.json is the Draft 2020-12 schema for loop-timing-witness.run-manifest.v1. The manifest declares the UTC start, source kind and tool, profile, placement, controller coefficients, plant and fixed-point format, sample period, run length and warm-up, load case, fault schedule, file paths and SHA-256 values, instrument state and operator notes. It includes a hash-bound copy of measurement-domain.json in the run directory; the host interprets event codes, rails and interval definitions from that verified copy. A board declaration additionally requires the hashed bitstream, firmware, Linux image and controller binary files, complete tracking and power files, an instrument floor, four acceptance flags, room temperature and board-only supply. Those declarations cannot substitute for actual board acceptance records; independent board qualification remains a separate gate. A report from such a declaration is labelled board_declaration_unverified until external qualification verifies the board evidence.

JSON input rejects nonfinite tokens (NaN, Infinity and -Infinity) and decimal or exponent values that overflow to a nonfinite number. This applies inside nested objects and arrays, including worker readiness and receipts. Finite numeric values and quoted strings naming those tokens remain valid.

The source loader accepts schema-valid UTC date-time spellings with upper- or lowercase T and Z, or an explicit +00:00 offset. Nonzero UTC offsets remain invalid. Parsing does not rewrite started_utc: the original value is retained in the loaded manifest and report.

The 16-byte event record is little-endian: event_type u8, zero reserved byte u8, zero reserved word u16, cycle u32 and timebase_ticks u64. The repository measurement-domain.json defines the current event codes and intervals; each run keeps its own verified snapshot. The decoder refuses partial records, nonzero reserved fields, unknown or wrong-profile codes, events outside the declared cycle range, non-monotonic cycle or tick order, and duplicate event kinds in one cycle. Its capture-module source and Icarus simulation testbench are under rtl/ and tests/rtl/.

The optional tracking CSV has the exact header cycle,reference,output and one finite decimal observation per analysed control cycle. reference and output have the unit named by plant.tracking_unit. The optional power CSV has the exact header timebase_ticks,rail,voltage_v,current_a,energy_j; energy is a cumulative joule counter, and all four rails from the measurement-domain contract must occur at each fabric tick. Voltage, current and energy are finite and nonnegative. The run manifest may declare either auxiliary file as null for an incomplete simulation run. A board run requires both files. Missing data remains unavailable in the report; it is never substituted.

Native simulation capture

.venv/bin/python tools/capture_native_simulation.py configuration.txt \
  --output-dir path/to/new-native-run --thermal 0

The command freezes the Makefile, controller C sources, native runtime and production RTL, then compiles and executes that snapshot. The complete configuration format is documented in PLANT_WITNESS.md. --thermal 1 compiles the thermal plant; zero selects the mechanical plant. Each run retains the executable, compiler versions, build and native logs, original binary events, raw controller trace and native-run.schema.json metadata. The metadata records the actual controller coefficients, reference, full fault duration, overload work and completion counters. The producer checks event count, missed deadlines and run length against the decoded capture. Failed runs retain their artifacts; an existing output directory is refused. Each capture also freezes the actual loaded parent project tools, Python package and packaged JSON contracts. Their original paths, resolved locations and SHA-256 values are recorded in producer_context.json, alongside the Python interpreter, platform and runtime dependency versions. The manifest binds that document and every retained source member through source.files. The producer verifies both original and frozen bytes after native execution and before writing its report; changed source locations, source bytes or context metadata refuse acceptance. The Python capture_simulation API also rejects plant modes outside zero/one and FIFO address widths outside one/fourteen before allocating a run. A native failure remains a failure even if event, trace and metadata files were completed; only the documented FIFO overflow exit produces an invalid report with retained observations.

Native runs declare tracking_sampling: observed. Their tracking rows correspond exactly to captured SAMPLE_READ cycles, including the warm-up prefix. Dropped reads are never filled. The analyzer reports observed sample count, expected cycle count, missing cycles and coverage fraction. Partial coverage remains invalid for a complete run; its RMS and peak describe only observed cycles and may be biased by missing samples. A header-only series is accepted only when the capture has no sample reads, and produces no error metric. Omitting tracking_sampling or declaring complete retains the original requirement for every analyzed cycle.

These runs remain simulation_only and contain no power series or hardware acceptance claims. The command forwards optional native --cpu N, --scheduler normal|fifo and --priority N. The hash-bound metadata records the actual startup policy and allowed CPU mask; see native scheduling. Host controller calculation does not advance model time; explicitly configured modeled overload advances it separately. They establish native/RTL integration, not physical processor latency.

Linux host load profiles

.venv/bin/python tools/capture_native_simulation.py configuration.txt \
  --output-dir path/to/new-loaded-run --thermal 0 \
  --load-profile memory --load-cpu 2 --load-working-set-bytes 1048576

Select a CPU from the launching process's actual allowed affinity. --load-profile and --load-cpu must occur together. Worker CPU selection is independent of the native --cpu option: select the same CPU for shared CPU contention, or separate allowed CPUs for those requested placements. No automatic native CPU isolation is applied. The five profiles are idle, cpu, memory, network and storage; without these options capture creates no extra worker. Buffers and working files are bounded between 4096 and 16777216 bytes; loopback UDP datagrams are limited to 60000 bytes. CPU arithmetic and idle do not allocate the requested buffer. The worker uses SCHED_OTHER, priority zero and the explicit CPU, and records kernel policy readback. It reports readiness after one completed workload chunk, runs while the native process executes, and is stopped and reaped before capture completes. It does not change the launching process's scheduling or affinity.

Memory work reads and modifies one byte per 64-byte stride. Network work sends and verifies private loopback UDP datagrams; it is not NIC traffic. Storage work writes, fsyncs and reads back an exclusive bounded working file; this does not establish uncached block-device load. The owned file and worker log remain in host_load_workspace/. Resource and native failures retain their artifacts and remain failures. The standalone linux_load.py CLI can wrap an explicitly selected absolute native executable with the same profiles, exclusive --journal and --workspace paths and a bounded --timeout.

The launcher owns both control and readiness pipes through worker_channels. It closes its child-end copies after process creation and the control writer when stopping the worker, so EOF does not depend on garbage collection. Process groups are stopped and reaped before remaining pipe resources are released. An already released descriptor is never closed again after its number is reused.

Readiness must be a complete UTF-8 JSON line within ten seconds and at most 4096 bytes including its newline. The reader uses one deadline across all pipe chunks, requires Boolean true and a positive integer PID, and verifies that PID against the owned worker. Partial lines, early EOF, oversized frames, repeated JSON member names and numeric substitutes for these fields are refused before native execution. The completed worker receipt uses the same strict JSON decoder, including repeated-member refusal inside nested counter objects.

Loaded captures freeze the host load implementation, including the worker, configuration, launcher, readiness reader, pipe ownership, process cleanup and shared strict JSON decoder sources. The parent package copy includes the actual JSON decoder implementation behind the compatibility shim; the worker keeps its separate frozen source copy. The manifest binds host_load.json, the worker receipt/log and optional storage data by SHA-256. The receipt follows host-load.schema.json, preserves actual native return code and PIDs, policy, operation totals and monotonic time brackets. The analyzer rejects inconsistent counters, resource scopes, CPU binding, run profile and native completion status. It copies the validated receipt to report.host_load and retains its digest. Counters cover the entire worker span, including work before and after the enclosed native interval. They do not establish uninterrupted activity or operation totals restricted to that interval. Host monotonic timestamps are not fabric event timestamps; fixed simulation clocks still establish no physical processor timing or power result.

The Python capture API accepts CaptureOptions(fifo_address_bits=8, host_load=...) for build capacity and optional LoadConfiguration. Invalid options are rejected before output allocation. The C/Rust/RTL controller arithmetic is unchanged; this workload lifecycle is implemented by the host Python tools.

Native completion and FIFO loss

Native captures declare an optional hash-bound native_metadata file in the run manifest. The analyzer validates its schema, source kind, controller coefficients, plant, cycle count and sample period against the run. The native fault kind, cycle and delay duration must also match the declared fault schedule, including an empty schedule for an unfaulted run. native_completion preserves the program's final live sample/record/miss/overflow/safe counters. Recorded event count and overflow must always agree with the receipt. With zero overflow, full-run deadline count, misses, sample reads and safe state must agree too. This check includes the warm-up prefix even when reported metrics exclude it. Existing manifests without a native receipt retain their original analysis path.

With declared overflow, captured-event misses can differ from live misses: losing ACT_WRITE can make an on-time command appear absent, and losing DEADLINE removes its observation. The report retains both values, states this inference limit and remains invalid. The receipt does not fill events or establish missing intervals. Malformed events, missing observed read records, or a lost declared fault anchor can still prevent report construction; the original artifacts remain available for diagnosis.

.venv/bin/python tools/capture_native_simulation.py configuration.txt \
  --output-dir path/to/new-overflow-run --fifo-address-bits 1

--fifo-address-bits selects the actual compiled FIFO capacity, from 2 to 16384 records; the default is 256. The source-bound build_parameters.json and build log retain the selected capacity, group queue, period and plant. The copied measurement-domain file retains the repository architecture contract; these experimental compile parameters describe the simulated instance. A confirmed overflow returns failure while retaining its manifest and invalid report when the surviving records can be analyzed. A failed native run without a valid completion receipt is not converted into a successful capture. No buffer experiment qualifies board acceptance or advances an instrument acceptance flag.

Calculations

Same-cycle event intervals are differences of captured fabric ticks. For CONTROL, a deadline is missed when no ACT_WRITE precedes that cycle's DEADLINE; a write at the deadline is late. Fault-detection latency is FAULT_DETECTED − FAULT_INJECTED. Time to safe state is measured from the first consecutive missed deadline to SAFE_STATE. A fault schedule must match the captured injection cycles. The event file is invalid for a complete run if a cycle anchor is absent or the FIFO overflow count is nonzero.

For every interval the tool reports the sample count, observation duration in ticks, median, p95, p99, p99.9 and maximum. Percentiles use linear interpolation at position (n − 1) × p in the sorted samples. Control error is the RMS and peak absolute value of reference − output after warm-up. Power uses differences of the four cumulative rail counters between consecutive fabric-tick samples. It divides the sum by the count of cycle anchors in those windows, reporting mean joules per cycle per rail and total, plus window and cycle counts. It makes no instantaneous per-cycle energy claim. A power series that does not cover every analysed cycle is refused. When warm-up cycles are discarded, the first contributing power window must start at or after the first analysed cycle; a window spanning warm-up and analysed cycles is refused.

Every report carries the run's source kind, input hashes, timebase resolution, instrument floor, sample counts, observation duration and stated limits. The current valid flag means the input series are complete and have no declared overflow; simulation_only remains simulation even when valid is true. Physical instrument acceptance and public measured claims require the separate hardware gates in MEASUREMENT_PROTOCOL.md.

Outputs

Latency jitter

The unreleased source analyser adds events.jitter and the method identifier absolute_cycle_to_cycle_latency_change.v1 to the existing analysis-report v1 summary. These derived fields do not change the event, domain or run-manifest input contracts. They are not present in the published Python 0.1.0 artefact.

For each periodic interval, let L[c] be its same-cycle latency in fabric ticks. The jitter sample for adjacent analysed cycles is abs(L[c] - L[c-1]). Both endpoints must be observed in both cycles. Warm-up cycles are excluded before pairing; missing cycles or endpoints never create a pair across a gap. This is a project-specific measure of cycle-to-cycle latency variation, distinct from deviation of the sampling period from its nominal value. Occasional fault-detection and safe-state intervals retain their latency distributions and do not receive periodic jitter statistics.

Jitter uses the same Type-7 median, p95, p99, p99.9 and maximum as latency. sample_count counts contributing adjacent pairs, not events or latency samples. latency_sample_count is separate. For N declared post-warm-up cycles, expected_pair_count is max(0, N-1); missing_pair_count and pair_coverage_fraction expose missing observations. Complete nonempty pairing is available, incomplete nonempty pairing is partial, and zero pairs are unavailable. No-pair quantiles are null, including a one-cycle window; observed constant latency has zero jitter. Partial distributions describe only observed pairs and can be biased by missing events. This status is independent of the report's existing input-validity and physical-evidence fields.

peak_to_peak_ticks reports max(L) - min(L) separately, with at least two latency observations. It can be available across a gap even when no adjacent pair exists; it never substitutes for the jitter distribution. cycle_jitter.csv retains both cycles, both latencies, signed delta_ticks and nonnegative absolute_delta_ticks for independent recalculation. All tick-valued fields convert to nanoseconds by multiplication with timebase_resolution_ns. Jitter CSV rows carry the run identity, evidence status, method, resolution and declared instrument floor; the summary also retains the observation duration. The floor is recorded without correction or subtraction. No simulated jitter qualifies a physical instrument or a pooled repeat campaign.

File Content
report.json Canonical JSON summary with evidence status, validity, metrics and limits
interval_summary.csv One row per interval with count, distribution and duration
cycle_intervals.csv Individual same-cycle intervals for audit and independent recalculation
jitter_summary.csv Per-interval absolute-change distributions, pair coverage and latency spread
cycle_jitter.csv Contributing adjacent-cycle pairs with signed and absolute latency changes
interval_p99.svg Labelled p99 comparison of available event intervals
jitter_p99.svg Labelled p99 absolute adjacent-cycle latency changes, including observed zeroes
energy_per_cycle.svg Rail comparison when a complete power series exists

Each CSV row and SVG title carries the evidence status, so a detached simulation artefact retains its provenance. The SVG charts are visual summaries. The JSON and CSV carry numeric results.

Linux native and RV64 builds

make uio-transport run-uio uses host GCC/G++ and writes to build/ by default. The separate Linux AMP collector is built with make run-amp-uio. These targets accept LINUX_CC, LINUX_CXX, LINUX_CPPFLAGS, LINUX_LDFLAGS and LINUX_BUILD_DIRECTORY. The run target compiles its controller C object in that same directory, so a target build can keep its objects separate from native simulation artifacts. Strict compiler warnings remain enabled for either placement.

For an RV64 Linux toolchain and its genuine target sysroot:

make uio-transport run-uio \
  LINUX_CC=/path/to/toolchain/bin/riscv64-linux-gnu-gcc \
  LINUX_CXX=/path/to/toolchain/bin/riscv64-linux-gnu-g++ \
  LINUX_CPPFLAGS="--sysroot=/path/to/target-sysroot" \
  LINUX_LDFLAGS="-L/path/to/target-sysroot/usr/lib/riscv64-linux-gnu" \
  LINUX_BUILD_DIRECTORY=build/rv64-linux

Supply target OpenSSL headers and libcrypto, including the architecture-specific generated OpenSSL headers, in the selected sysroot. SDK layouts may require extra target include paths in LINUX_CPPFLAGS. A relocated compiler may also require its own host tool libraries in its execution environment; these are separate from the target libraries passed to the linker. Check the resulting ELF machine, ABI, loader and dynamic dependencies with the target readelf. Cross compilation establishes a target build, not execution on the board or compatibility with an unverified board image. The board's actual libc, loader and OpenSSL versions must be checked before deployment.

Native PAC1934 acquisition journal

Build with make run-uio. The native UIO command accepts --power-config /path/to/power.conf --power-journal /path/to/power-journal.csv together, plus required --metadata /path/to/native_metadata.json and an explicit --cpu for the controller. The worker CPU must be a different CPU within the process's original allowed affinity. The worker sets and reads back SCHED_OTHER and its single CPU before collecting. Simulation rejects these physical acquisition options.

The whitespace configuration contains:

pac1934-iio-mchp-v1 iio:deviceN exact_kernel_release
period_ns maximum_read_ns worker_cpu sample_rate
VDD channel shunt_microohms label_prefix
VDD25 channel shunt_microohms label_prefix
VDDA25 channel shunt_microohms label_prefix
VDDA channel shunt_microohms label_prefix

Replace every symbolic field with the actual board configuration. No board, shunt, rail label, or kernel defaults are inferred. Channels 1–4 form a permutation; sample rate is 8, 64, 256 or 1024 samples/s. Polling period is 50 ms–60 s and the positive maximum frame read span cannot exceed that period. Labels, exact in_shunt_resistor1–in_shunt_resistor4 values and kernel identity are verified against actual read-only sysfs; accumulators must already be enabled. The logger does not configure or reset the monitor.

The selected Microchip driver source exports unsigned voltage/current raw words and accumulated energy already scaled by sample rate. Its fractional scales use nine-decimal truncation. Raw × scale yields millivolts, milliamperes and millijoules respectively; energy must not be divided by sample rate again. Bipolar scale configurations are refused by this unsigned acquisition contract. The exact kernel release and these attribute semantics must be checked against the installed BSP.

Each CSV row records snapshot, rail, channel, quantity, raw/scale attribute, host nanoseconds before/after, fabric ticks before/after, observed worker policy, configured kernel release, rail label, shunt, sample rate and exact attribute text. Frames are collected before START, periodically while the controller runs, and after final drain. A frame comprises separate reads and includes identity/configuration checks before and after. Reset/decreasing or saturated energy, changed calibration, clock reversal, excessive read span, missed polling schedule and output failures stop the run with partial files retained. Output paths are exclusive. Worker shutdown joins before UIO/IIO resources are released; kernel I²C read timeouts determine the actual shutdown bound.

The driver's 50 ms cache and sequential reads prevent an atomic four-rail timing claim. This journal is not directly accepted as a qualified analyzer power file. Physical calibration, read/cache timing, rail mapping and interval alignment require the board. The implemented public refusal paths and builds can be checked here; actual successful acquisition remains unverified because no board or monitor is available.

Native artifact receipts

Both native run executables link OpenSSL 3 libcrypto (development headers are required to build). Native metadata records crypto_library with SHA-256 algorithm, compile-time header version number and OpenSSL_version_num() from the actually loaded library; these may differ after a shared-library update. Native controller kernels remain dependency-free; the Linux/simulation run adapter uses the EVP SHA-256 interface for capture provenance. OpenSSL 3 is dynamically linked under Apache-2.0; its source is not vendored into the controller kernels. The installed host reports OpenSSL 3.0.13; this records the local library version, not a target BSP qualification.

With --metadata, the native receipt contains SHA-256 and exact byte counts for configuration, events and raw tracking, plus power configuration and the completed raw journal when acquisition is enabled. Configuration bytes are checked before/after parsing and after execution. Hashes of closed outputs are calculated after final drain, outside the controller service loop. Files must be stable regular files; symlinks, nonregular paths and detected replacement/modification during hashing are refused. Failures preserve partial capture files.

If configuration bytes change during execution, the completed event and raw tracking files remain available, but the command fails before publishing native metadata or a success summary. The retained files alone are not a verified capture. Real simulation tests synchronize on Linux output-creation notifications, confirm their own child is stopped, modify the actual configuration, and resume it. They cover overwrite, inode replacement, appended whitespace and symlink substitution for both plants; they do not substitute a hash implementation or filesystem stream. Separate initial-hash tests observe actual Linux read notifications, stop their own child and verify its open configuration descriptor has read some but not all bytes. Append, truncation, path replacement and unlink then exercise refusal before any capture outputs are created. A Linux read-error case uses /proc/self/mem at offset zero: the kernel returns EIO for the process's own unmapped address. The native command reports the read failure before creating outputs. This is an actual kernel error test; no memory bytes are printed or used as capture data.

The simulation importer compares native event/raw receipts against actual bytes before constructing a manifest; raw conversion interprets the verified bytes. The configuration receipt must match a captured source artifact. Analyzer completion validation also checks the event receipt against the manifest and decoded byte count. A matching receipt does not establish physical power timing or board acceptance. Metadata without artifact receipts comes from an earlier development draft and must be regenerated before import under this schema.

Native raw conversion validates all 13 ABI columns: cycle, six signed 32-bit Q8.24 values, three boolean flags and three unsigned 64-bit timing/work counters. Fields must be nonempty ASCII decimal integers within their bounds. Observed cycles must be unique and increasing; gaps remain valid observations. Header, row completeness and native sample count are checked before writing any converted tracking file. Hash agreement does not replace these semantic checks.

Malformed CSV is reported as a validation error. Conversion uses an isolated Decimal context with enough precision for every signed 32-bit Q8.24 value; caller precision/trap settings cannot round an observation. Exact converted values are checked against independent rational arithmetic.

The power journal uses the same exclusive stream owner as native event and tracking outputs. Its constructor flushes the header before starting acquisition, and its controller-thread lifecycle refuses another start after stop or close, finish before start, and another finish after close. Worker join precedes stream destruction. Shared stream ownership is exercised with actual RTL capture and real files; positive PAC1934 worker lifecycle verification still requires the physical device.

Both the configuration parser and public C++ run API validate cycles, period, coefficients, reference/phase bounds, fault schedule and overload bounds, and the nanosecond duration limit. configure_run performs this validation before its first device access, so an invalid public structure cannot reset an earlier completed run or erase its retained register state. Actual RTL API tests retain a completed capture and register/IRQ-generation snapshot, reject each invalid configuration, verify the snapshot is unchanged, then complete another healthy run.

The public C++ scheduling API shares option validation with the CLI parser. Invalid CPU, scheduler or priority sentinels and incomplete FIFO/power requests are refused before reading or changing policy. Real child-process API tests start in Linux SCHED_BATCH, prove refusal preserves scheduler, priority, nice value and affinity, and exercise inherited-policy retention and explicit normal-policy pinning. These host policy observations do not qualify real-time latency.

The public metadata writer also validates configuration before creating its exclusive file. Malformed cycle counts or fault enums are refused before output creation and fault-name lookup. Actual completed-run API tests prove refusal leaves the metadata path available, then write the original completion receipt with SHA-256 and byte counts of the retained configuration, events and raw tracking. These receipts are checked against the native metadata schema.

Power acquisition shares semantic validation between its file parser and public C++ structures. The PAC1934 constructor validates identity syntax, polling bounds, CPU, supported sample rates, rail order, distinct channels, positive bounded shunts and journal-safe labels before inspecting the kernel or selecting an IIO node. The journal validates the supplied structure before creating its exclusive output. Actual constructor refusal tests submit malformed structures without inventing sysfs devices; positive acquisition and worker lifecycle still require a physical board. Public raw/scale reads require the supplied rail to match the constructor's verified mapping, including channel, label and shunt. A different caller-supplied rail is refused before attribute access or scale arithmetic; that physical-object boundary remains unexercised without the device.