Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions .github/actions/setup-pybnf/action.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
name: Set up pybnf (bngsim-less)
name: Set up pybnf
description: >-
Install BioNetGen (BNG2.pl) plus pybnf and its public runtime/test
dependencies into a uv venv, deliberately omitting the private, macOS-only
bngsim wheel. Tests run under PYBNF_NO_BNGSIM=1 so bngsim-marked tests skip
(see tests/conftest.py). BNG2.pl is still required: Configuration validation
execs `BNG2.pl -v` for any BNGLModel regardless of bngsim, and the analytical
tests aside, most of the suite builds such configs.
Install BioNetGen (BNG2.pl) plus pybnf and its runtime/test dependencies into
a uv venv. bngsim comes from public PyPI by default (`bngsim` input), so the
bngsim-marked suites actually run on hosted runners; pass `bngsim: 'false'`
for a leg that must stay bngsim-less (a python with no cp wheel, or a job
that does not simulate at all), and run it under PYBNF_NO_BNGSIM=1 so the
bngsim-marked tests skip cleanly (see tests/conftest.py). BNG2.pl is required
either way: Configuration validation execs `BNG2.pl -v` for any BNGLModel
regardless of bngsim, and the analytical tests aside, most of the suite
builds such configs.

inputs:
python-version:
description: Python version for the venv (e.g. "3.11", "3.12").
required: true
bngsim:
description: >-
Install bngsim (from public PyPI) alongside the rest of the deps.
Default "true". Set "false" on a leg where bngsim must be absent:
bngsim publishes manylinux_2_28_x86_64 wheels for cp310-cp313 only, so a
newer interpreter would fall back to building the sdist (C++17 toolchain,
CMake, Ninja) -- and jobs that never simulate (docs) do not need it. A
"false" leg must also set PYBNF_NO_BNGSIM=1 when it runs pytest.
required: false
default: 'true'
petab-spec:
description: >-
The petab requirement to install. Defaults to stock PyPI petab. A CI leg
Expand Down Expand Up @@ -70,24 +83,39 @@ runs:
tar -xzf "$RUNNER_TEMP/bionetgen.tar.gz" -C "$RUNNER_TEMP"
echo "BNGPATH=$RUNNER_TEMP/BioNetGen-2.9.3" >> "$GITHUB_ENV"

- name: Create venv and install pybnf without bngsim
- name: Create venv and install pybnf
shell: bash
env:
BNGSIM: ${{ inputs.bngsim }}
PETAB_SPEC: ${{ inputs.petab-spec }}
run: |
# Build the venv from the setup-python interpreter (see note above), not
# a uv-managed one.
uv venv --python "${{ steps.python.outputs.python-path }}"
# Mirror pyproject.toml's runtime deps MINUS bngsim (private, mac-only
# wheel, not on PyPI), plus the test stack (pytest, hypothesis,
# pytest-xdist) and petab. petab is BOTH the PEtab v2 export/BnglModel
# oracle (ADR-0025/0026 -- BNG2.pl is installed above, so model-level
# validation runs here too) AND the runtime math translator backing the
# expression observableFormula path (pybnf[petab], ADR-0035, petab.v2.math);
# stock PyPI petab carries the math grammar, so one install covers both.
# The exact petab requirement is the `petab-spec` input (default stock
# PyPI; a CI leg overrides it with the Step B fork to run the NATIVE
# BNGL loader). Keep these version bounds in sync with pyproject.toml;
# bngsim is the only intentional omission.
# bngsim is on public PyPI (manylinux_2_28_x86_64 + macOS + win wheels,
# cp310-cp313), so hosted runners get the real simulation backend --
# NFsim, RuleMonkey, libsbml, codegen and output sensitivities are all
# compiled into the published wheel. The [antimony] extra is the one
# capability that needs a companion PyPI package (antimony>=3.1.1, pure
# python wheel), and it is what unskips the bngsim_antimony-marked
# tests, so install the extra rather than bare bngsim. Keep the pin in
# sync with pyproject.toml.
BNGSIM_SPEC=()
if [ "$BNGSIM" = 'true' ]; then
BNGSIM_SPEC=('bngsim[antimony]>=0.11.35,<1')
fi
# Mirror pyproject.toml's runtime deps, plus the test stack (pytest,
# hypothesis, pytest-xdist) and petab. petab is BOTH the PEtab v2
# export/BnglModel oracle (ADR-0025/0026 -- BNG2.pl is installed above,
# so model-level validation runs here too) AND the runtime math
# translator backing the expression observableFormula path
# (pybnf[petab], ADR-0035, petab.v2.math); stock PyPI petab carries the
# math grammar, so one install covers both. The exact petab requirement
# is the `petab-spec` input (default stock PyPI; a CI leg overrides it
# with the Step B fork to run the NATIVE BNGL loader). Keep these
# version bounds in sync with pyproject.toml.
uv pip install \
"${BNGSIM_SPEC[@]}" \
'dask>=2024.1.0' \
'distributed>=2024.1.0' \
'libroadrunner>=1.6.0' \
Expand All @@ -101,7 +129,9 @@ runs:
'pytest>=7,<10' \
'hypothesis>=6,<7' \
'pytest-xdist' \
'${{ inputs.petab-spec }}'
# Install pybnf itself without re-resolving deps (which would pull
# bngsim and fail).
"$PETAB_SPEC"
# Install pybnf itself without re-resolving deps: on a bngsim: 'false'
# leg a re-resolve would pull bngsim in through pyproject.toml and
# defeat the point, and everywhere it would re-derive a dependency set
# this step just pinned.
uv pip install --no-deps .
14 changes: 9 additions & 5 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,18 @@ jobs:
steps:
- uses: actions/checkout@v6

# Reuse the tests' composite action: it installs pybnf + its PUBLIC runtime
# deps into a uv venv WITHOUT the private, mac-only bngsim wheel -- exactly
# the environment autodoc needs to import pybnf on Linux. (It also fetches
# BNG2.pl, unused here but harmless; the win is one source of truth for the
# dependency list.)
# Reuse the tests' composite action: it installs pybnf + its runtime deps
# into a uv venv -- exactly the environment autodoc needs to import pybnf
# on Linux. (It also fetches BNG2.pl, unused here but harmless; the win is
# one source of truth for the dependency list.)
- uses: ./.github/actions/setup-pybnf
with:
python-version: '3.12'
# autodoc only imports pybnf; it never simulates. Skip the bngsim
# install and build under PYBNF_NO_BNGSIM=1 (below) so the docs job
# stays lean -- and so a docs build keeps proving that pybnf imports
# cleanly with no backend present.
bngsim: 'false'
# Distinct uv cache namespace so this job does not race the tests /
# lint jobs' content-addressed cache saves on the same push.
cache-suffix: docs
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: integration
# Manual only. The slow tier is the 4 statistical-recovery tests against
# analytical targets (AnalyticalModel) -- bngsim-free and BNG2.pl-free, ~8 min.
#
# NOTE: the bngsim *simulation* suites (true NFsim/RuleMonkey/SBML runs) are
# deliberately NOT in hosted CI -- they need the private, macOS-only bngsim
# wheel. They remain covered by the local pre-push hook (.pre-commit-config.yaml)
# on a dev machine until bngsim ships Linux wheels.
# NOTE: the fast bngsim *simulation* suites (true NFsim/RuleMonkey/SBML runs)
# DO run in hosted CI -- tests.yml installs bngsim from public PyPI (#514). Not
# covered anywhere hosted yet: the `recovery` tier (real parameter-recovery fits
# through the backend, over an hour), which still runs on a dev machine.
#
# Runs nightly and on manual dispatch.
on:
Expand All @@ -27,6 +27,10 @@ jobs:
- uses: ./.github/actions/setup-pybnf
with:
python-version: '3.12'
# The slow tier is analytical (AnalyticalModel): no simulation
# backend, so no bngsim install, and PYBNF_NO_BNGSIM=1 below keeps it
# honest about that.
bngsim: 'false'

- name: Run slow analytical recovery tests
env:
Expand Down
52 changes: 40 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,49 @@ jobs:
# Every supported version (requires-python >= 3.11; classifiers list
# through 3.14). CI is free on this public repo, so we test each claimed
# version rather than just the endpoints.
python-version: ['3.11', '3.12', '3.13', '3.14']
name: pytest (py${{ matrix.python-version }})
include:
- python-version: '3.11'
- python-version: '3.12'
- python-version: '3.13'
# bngsim publishes cp310-cp313 wheels, so 3.14 would fall back to
# building the sdist (C++17 toolchain, CMake >= 3.20, Ninja) inside
# the pytest job. Keep this leg deliberately bngsim-less instead: it
# is the guard that the bngsim-free code paths still work (a bare
# `pip install pybnf` on a machine with no wheel), which is worth a
# leg of its own regardless. Drop `no-bngsim` here the day a cp314
# wheel ships.
- python-version: '3.14'
no-bngsim: '1'
name: pytest (py${{ matrix.python-version }}${{ matrix.no-bngsim == '1' && ', bngsim-less' || '' }})
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-pybnf
with:
python-version: ${{ matrix.python-version }}
bngsim: ${{ matrix.no-bngsim == '1' && 'false' || 'true' }}

- name: Run unit tests (bngsim-less, parallel)
- name: Run unit tests (parallel)
env:
# bngsim is a private mac-only wheel; with it absent, conftest.py
# auto-skips bngsim/nfsim/sbml/etc.-marked tests. The remaining suite
# runs against the bngsim-less code paths (and BNG2.pl from the
# composite action). -n auto parallelizes across the runner's cores.
PYBNF_NO_BNGSIM: '1'
# Empty except on the bngsim-less leg, where it is '1'. An empty value
# reads as unset (pybnf/_bngsim_caps.py tests truthiness), so bngsim
# is imported and the bngsim/nfsim/rulemonkey/sbml/antimony-marked
# tests run for real; with '1' conftest.py auto-skips them.
PYBNF_NO_BNGSIM: ${{ matrix.no-bngsim }}
# -m: the default addopts expression is `not slow and not recovery`, and
# naming -m here REPLACES it -- so both tiers must be restated. Omitting
# `not recovery` used to be harmless because 129 of the 130 recovery
# tests skip without bngsim; with bngsim installed they are real
# parameter-recovery fits through the backend and the job would run for
# over an hour. (The cost of deselecting: the 130th, a roadrunner-backed
# Boehm oracle in test_petab_sbml_layer.py, is bngsim-free and no longer
# runs here.) -n auto parallelizes across the runner's cores.
#
# --no-sync: do NOT let `uv run` re-resolve the project env from
# pyproject.toml (that would pull the private bngsim and fail). The
# composite action already populated .venv.
run: uv run --no-sync pytest -m "not slow" -n auto
# pyproject.toml -- the composite action already populated .venv with an
# exact dependency set (and on the bngsim-less leg a re-resolve would
# pull bngsim right back in).
run: uv run --no-sync pytest -m "not slow and not recovery" -n auto

# The PEtab v2 export oracle (ADR-0025/0026) validates emitted `language: bngl`
# problems at MODEL level. On the pytest job above (stock PyPI petab) that runs
Expand All @@ -51,6 +74,11 @@ jobs:
- uses: ./.github/actions/setup-pybnf
with:
python-version: '3.12'
# This leg is about petab's loader, not simulation: the two oracle
# files it runs are backend-free, so skip the bngsim install (and run
# under PYBNF_NO_BNGSIM=1 below) rather than pay for a wheel nothing
# here exercises. The matrix legs above cover bngsim.
bngsim: 'false'
petab-spec: 'petab @ git+https://github.com/wshlavacek/libpetab-python.git@bngl_model_support'
# This job also runs 3.12 through setup-pybnf, so without its own
# suffix it shares the matrix py3.12 job's cache key and one save loses
Expand All @@ -71,4 +99,4 @@ jobs:
# `language: bngl` through petab's own native BnglModel here (the
# register_bngl() monkeypatch collapses to a no-op). This is where the
# TestRegisterBngl native-no-op test actually runs instead of skipping.
run: uv run --no-sync pytest -m "not slow" tests/test_petab_export.py tests/test_petab_import.py
run: uv run --no-sync pytest -m "not slow and not recovery" tests/test_petab_export.py tests/test_petab_import.py
33 changes: 19 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@
# `ruff@0.15.14 check .`). Runs FIRST and takes seconds, so a lint failure
# aborts the push immediately instead of after the pytest gate (the pytest
# hook does NOT run ruff -- that gap let an F401 reach CI once).
# 2. pytest-bngsim -- the bngsim (mac-only) tests CI *cannot* run, since the
# private bngsim wheel is absent on hosted runners. This is the gate's
# unique value: every other test already runs in CI. Run with `-n auto`
# so the whole gate finishes in well under a minute.
# 2. pytest-bngsim -- the simulation-backend tests, as a fast local smoke.
# These are no longer CI-exclusive: bngsim is on public PyPI and tests.yml
# installs it, so hosted runners now run these files too (#514). The hook
# stays because it is the shortest path from "I broke the backend seam" to
# knowing it -- seconds locally instead of a CI round-trip. Run with
# `-n auto` so the whole gate finishes in well under a minute.
#
# Deliberately NOT here: the bngsim-less suite (`PYBNF_NO_BNGSIM=1 pytest
# -m "not slow" -n auto`). CI's tests.yml runs that exact command on every push,
# so mirroring it locally added minutes to the gate for zero extra coverage --
# and a multi-minute pre-push hook holds the push's SSH connection idle long
# enough for GitHub to reap it ("Connection to github.com closed by remote
# host"), so the push silently never lands. Keeping the gate fast is what makes
# `git push` actually complete. Bngsim-less regressions still surface in CI.
# Deliberately NOT here: the rest of the suite (`pytest -m "not slow and not
# recovery" -n auto`, ~3300 tests). CI's tests.yml runs that on every push, so
# mirroring it locally added minutes to the gate -- and a multi-minute pre-push
# hook holds the push's SSH connection idle long enough for GitHub to reap it
# ("Connection to github.com closed by remote host"), so the push silently never
# lands. Keeping the gate fast is what makes `git push` actually complete.
#
# Also NOT here: the `recovery` tier (real parameter-recovery fits through the
# backend, over an hour). That one runs nowhere automatically -- run it by hand
# when a change touches the fitting loop.
#
# `uv run --no-sync` uses the project's interpreter without touching the
# environment -- the .venv is already populated and uv.lock isn't fully
Expand All @@ -40,9 +45,9 @@ repos:
stages: [pre-push]
- id: pytest-bngsim
name: pytest (bngsim test suite)
# Every bngsim-area test file: bngsim is mac-only and absent in CI, so
# these run nowhere else. `-n auto` parallelizes across cores to keep the
# gate fast. Add files here as bngsim coverage expands.
# Every bngsim-area test file, as a pre-push smoke (CI runs them too --
# this is the fast local copy). `-n auto` parallelizes across cores to
# keep the gate fast. Add files here as bngsim coverage expands.
entry: >-
uv run --no-sync pytest -m "not bionetgen" -n auto
tests/test_bngsim_bridge.py
Expand Down
7 changes: 5 additions & 2 deletions tests/README_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ pytest -m recovery -k m03 # just one model

Requirements / behaviour:

- **bngsim** must be installed (the macOS-only C++ wheel) — auto-skips via the
`bngsim` marker otherwise, so this never runs in hosted CI.
- **bngsim** must be installed (public PyPI: `pip install bngsim`) — auto-skips
via the `bngsim` marker otherwise. Hosted CI installs bngsim and runs the
bngsim-marked suites, but explicitly deselects this tier
(`-m "not slow and not recovery"`): the fits across all 16 recovery-marked
files run well over an hour. It stays a dev-machine tier.
- **BNG2.pl** must be resolvable (via `BNGPATH`): bngsim is a simulation engine,
not a network generator, so rules→`.net` expansion runs once per fit at setup.
`recovery_harness.require_bng2pl` skips the tier if it isn't found.
Expand Down
3 changes: 3 additions & 0 deletions tests/test_gradient_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -2980,6 +2980,9 @@ def test_fd_acceptance_gate_estimated_scale(model_kind):
chain rule end-to-end on the real bngsim decay net. The scale never enters the data fit, so its
whole gradient comes from the constraint's scalar column -- the estimated-noise pattern, applied
to a qualitative constraint."""
# ``s_q`` is loguniform, so the scale column's log-space chain rule autodiffs through jax
# (the optional pybnf[jax] extra) -- same declaration as every other log-scale leg here.
pytest.importorskip('jax')
obj = ChiSquareObjective()
sigma = 5.0
model_name = 'e2e_ode_decay'
Expand Down
Loading