ferrotherm 0.24.0

Thermodynamic computing in pure Rust: sparse energy-based models, chromatic block-Gibbs, parallel tempering, thermodynamic linear algebra, stochastic differentiable programs, a variational compiler onto device topologies, exact inference by variable elimination, planted instances with known optima, sampler certificates, and a first-class joules ledger. std-only, zero dependencies, wasm-clean, deterministic by seed.
Documentation
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      # The cross-binding gates below compare SURFACES, so this job needs the toolchains those
      # surfaces are written in -- otherwise they skip, exit 0, and report success having compared
      # four of seven. That is the same shape as check-exports.sh sitting in a job with no Julia:
      # a gate that runs and covers less than it appears to. Measured before fixing: with a
      # CI-like PATH, check-answers.sh compared 5 and silently skipped julia and zig.
      - uses: julia-actions/setup-julia@v2
        with:
          version: "1.12"
      - uses: mlugg/setup-zig@v2
        with:
          version: 0.16.0
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - name: Playwright, for the wasm surface
        working-directory: web-tests
        run: npm install && npx playwright install --with-deps chromium
      - name: Install icarus-verilog (RTL bit-exactness gate)
        run: sudo apt-get update && sudo apt-get install -y iverilog
      - name: One version across every package
        run: scripts/check-versions.sh
      - name: Every C ABI symbol reaches every binding, or the gap is written down
        run: scripts/check-parity.sh
      # Parity proves the names exist; this proves they compute the same thing. Bindings whose
      # toolchain the runner lacks skip; a binding that is present and produces nothing FAILS.
      - name: Every binding compiles one model to the same bytes
        env:
          FERROTHERM_REQUIRE_ALL: "1"
        run: scripts/check-semantics.sh
      # And SOLVES it to the same answer, which is a different question. check-semantics compares
      # the model BEFORE it is solved, and its harness calls compile() explicitly -- so it exercised
      # the exact step Zig's `solve` was skipping, and stayed green while that binding could not
      # solve anything at all.
      - name: Every binding solves one model to the same answer
        env:
          FERROTHERM_REQUIRE_ALL: "1"
        run: scripts/check-answers.sh
      # Parity proves a symbol is DECLARED in each binding. For Julia that is an @cfn line, which
      # says nothing about whether a function wraps it -- both OMMX functions were once declared and
      # exported with no body in between, and the module loaded fine.
      - name: Unit tests
        # --workspace, not the root package alone. This was `cargo test --release`, which builds
        # and tests only `ferrotherm` -- so ferrotherm-serve, ferrotherm-cloud and
        # ferrotherm-silicon were never compiled by CI at all, and their tests never ran.
        run: cargo test --release --workspace
      # The tool that found the 0.12.0 headline defect ran in no job and no script. It aborted on
      # ffi.rs's deny-by-default lint, so five of the six crates and all 21 examples had never been
      # linted at all. The three lints this codebase deliberately does not follow are recorded in
      # Cargo.toml's [workspace.lints.clippy] with the reason for each; everything else is denied.
      - name: Clippy, across the whole workspace
        run: cargo clippy --release --workspace --all-targets -- -D warnings
      # THE GPU PATH IS EXECUTED HERE, not merely compiled.
      #
      # This step used to run `cargo test -p ferrotherm-gpu` on a runner with no adapter, where
      # every hardware-gated test skips. That caught an API break and verified no physics at all:
      # the fastest sampler in the stack had ZERO CI coverage, and its correctness rested on
      # whichever machine somebody remembered to run by hand. A second vendor then found a SIGSEGV
      # that had been latent for releases.
      #
      # lavapipe is a software Vulkan implementation, so a hosted runner can execute the real
      # shader. It says nothing about speed and everything about correctness, which is exactly what
      # a gate should assert -- the same split the README already makes for the DX12/WARP run.
      - name: Install lavapipe, a software Vulkan device
        run: sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers
      - name: GPU backend, actually run against a software Vulkan adapter
        run: |
          icd=$(ls /usr/share/vulkan/icd.d/lvp_icd*.json 2>/dev/null | head -1)
          test -n "$icd" || { echo "::error::mesa-vulkan-drivers installed but no lvp ICD found"; exit 1; }
          echo "using $icd"
          out=$(VK_ICD_FILENAMES="$icd" cargo test --release -p ferrotherm-gpu 2>&1)
          echo "$out"
          # A SKIP IS NOW A FAILURE. A driver was installed on purpose, so "no GPU adapter" means
          # it did not load -- and the shader went unverified while the job stayed green, which is
          # the exact shape of the hole this step was added to close.
          if grep -q "no GPU adapter" <<<"$out"; then
            echo "::error::lavapipe is installed but wgpu found no adapter; the GPU path did NOT run"
            exit 1
          fi
          # Name the test rather than counting them: a count goes stale the moment one is added,
          # and this is the one that puts the whole conformance suite through the GPU.
          grep -q "conform_can_finally_score_the_gpu_path ... ok" <<<"$out" || {
            echo "::error::the conformance case did not run on the GPU path"
            exit 1
          }
      # Same shape: no macmon on a Linux runner, so the measuring tests skip and the parsing and
      # refusal logic -- which is most of the crate -- still runs.
      - name: Power meter builds, and its tests skip rather than fail without a backend
        run: cargo test --release -p ferrotherm-meter
      # The OMMX bridge hand-rolls protobuf, so the check that matters is the REFERENCE
      # implementation reading what it writes. Installed here on purpose; the test skips without it.
      - name: OMMX bridge, checked against the reference implementation
        run: |
          python3 -m pip install --quiet ommx
          OMMX_PYTHON=python3 cargo test --release --test ommx_reference
      - name: Examples build
        run: cargo build --release --examples
      # RUN them, not merely build them.
      #
      # This step was two examples while `cargo build --examples` compiled all twenty, so eighteen
      # were checked for compiling and never for working. `dtm_scale` had been panicking on a
      # missing argument the whole time and nothing could see it, because a panic at runtime is
      # invisible to a build.
      #
      # Every example that runs unattended is run, and a non-zero exit fails the job -- which is
      # what the seven gate examples use to report a physics check failing. 115 s for seventeen of
      # them, measured, which is affordable on every push.
      #
      # Three are excluded by name, with the reason, rather than by a silent filter:
      #   depth_vs_dimension  199 s   too slow for every push
      #   reach_on_z1         107 s   the flagship; slow, and run before a release
      #   dtm_scale           needs a Fashion-MNIST idx3-ubyte file this runner does not have
      # docs.rs is a published surface and nothing here gated it. Turned on with 22 broken
      # intra-doc links already in the tree -- most of them prose like `[0,1]` or `reals[x]` that
      # rustdoc read as a link and could not resolve, two of them real (`Verdict::Runnable` and
      # `crate::graph::GRAPH_BUILDS` name items that do not exist). All fixed in the same commit
      # that added this step, because a gate turned on over a failing tree is a gate that gets
      # turned off again.
      - name: The published documentation has no broken links
        run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps

      - name: Every unattended example runs, and a gate example failing fails the build
        # FERROTHERM_ALLOW_BUSY, deliberately. `flips_bench` and `parity_bench` refuse to print a
        # throughput rate above a load average of 2, because a rate taken under contention measures
        # the run queue -- and a hosted runner that has just finished `cargo build --release
        # --examples` is exactly that. What this step checks is that the examples RUN, not what they
        # measure, so the guard is overridden here and the numbers from this job are not quotable.
        # The override is not silence: both print the load average and a caveat above their tables.
        env:
          FERROTHERM_ALLOW_BUSY: "1"
        run: |
          set -euo pipefail
          skip="depth_vs_dimension reach_on_z1 dtm_scale"
          ran=0
          for f in examples/*.rs; do
            n=$(basename "$f" .rs)
            case " $skip " in *" $n "*) echo "  skipped $n"; continue;; esac
            echo "  running $n"
            cargo run --release --quiet --example "$n" >/dev/null
            ran=$((ran + 1))
          done
          # A floor: if the glob ever stops matching, this passes over nothing.
          if [ "$ran" -lt 15 ]; then
            echo "only $ran examples ran; this checked almost nothing" >&2
            exit 2
          fi
          echo "  $ran examples ran, all exit 0"
  # Do the tests have teeth? A green suite says the code runs, not that a wrong answer would be
  # noticed -- and the 0.12.0 release exists because 320 green tests could not tell an invented LP
  # bound from a correct one. This breaks the code on purpose and requires the named test to go red.
  mutation:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Every recorded mutation is caught by the test named for it
        run: scripts/mutation-suite.sh

  c-abi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Build the library the header describes
        run: cargo build --release
      - name: The header compiles and links against it
        run: |
          cc -I include include/check.c -L target/release -lferrotherm -o /tmp/check
          LD_LIBRARY_PATH=target/release /tmp/check

  python:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python: ["3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-python@v5
        with:
          # The range we CLAIM, not one point in it. requires-python said >=3.9 while CI tested a
          # single version, so the floor was a claim nobody had ever run.
          python-version: "${{ matrix.python }}"
      - name: Build the library the bindings load
        run: cargo build --release
      - name: Python tests
        run: |
          pip install pytest
          python -m pytest python/test_model.py python/test_readme.py -q

  wasm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - name: wasm cdylib builds clean
        run: cargo build --release --lib --target wasm32-unknown-unknown
      # Not a byte comparison against a fresh build: a wasm binary is not reproducible across
      # toolchain versions, so that fails on a runner whose rustc differs from the author's and
      # says nothing about whether the artefact works. What matters is that every symbol the pages
      # reach for is present -- a missing export makes the call `undefined`, and calling undefined
      # in a click handler leaves the page looking fine and doing nothing.
      - name: the committed wasm exports everything the pages call
        run: scripts/check-wasm-exports.sh

  zig:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: mlugg/setup-zig@v2
        with:
          version: 0.16.0
      # `zig build test` runs cargo itself and links with an rpath, so there is one command here
      # rather than a hand-written link line that can drift from what a consumer would use.
      - name: Zig tests
        working-directory: zig
        run: zig build test --summary all

  julia:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: julia-actions/setup-julia@v2
        with:
          version: "1.12"
      - name: Build the library the binding loads
        run: cargo build --release
      # Moved here from the `test` job, which has no Julia -- so it printed "skipping", exited 0,
      # and had never checked anything in CI while reading as a step that passed. REQUIRE_JULIA
      # makes the skip a failure, so it cannot go quiet again.
      - name: Every exported binding name resolves
        env:
          FERROTHERM_REQUIRE_JULIA: "1"
        run: scripts/check-exports.sh
      - name: Julia tests
        working-directory: julia/Ferrotherm
        env:
          FERROTHERM_LIB: ${{ github.workspace }}/target/release/libferrotherm.so
        run: julia --project=. -e 'using Pkg; Pkg.test()'

  editor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - name: Install
        working-directory: web-tests
        run: npm install && npx playwright install --with-deps chromium
      - name: Browser tests
        working-directory: web-tests
        run: npm test