Development

This page documents the expected contributor workflow. The most important rule is that code, tests, and docs should move together: public behavior changes should not land without tests and documentation.

Environment

python -m pip install -e '.[yaml,viz,graph,docs]'
python -m pip install pre-commit
pre-commit install

If the pre-commit console script is not on PATH, use python -m pre_commit ....

Required checks

Run these before committing:

pytest -q
mypy src
python -m compileall -q src tests
pre-commit run --all-files
sphinx-build -W --keep-going -b dirhtml docs docs/_build/dirhtml

The pre-commit configuration currently runs merge-conflict checks, YAML checks, end-of-file fixes, trailing whitespace trimming, isort, mypy on src, and black.

Type checking covers src, as in CI and pre-commit. The tests are not held to it: many predate the annotation policy below, and validation tests pass deliberately mistyped values, so mypy tests reports errors. New tests should still be annotated and should wrap intentionally invalid arguments in typing.cast(Any, ...).

Documentation checklist

When changing public behavior, update docs in the same change:

  • Public Python API change: update API reference.

  • New or changed config field: update Configuration.

  • New or changed CLI command/option/artifact: update CLI reference.

  • New workflow or recommended usage: update Getting started or Examples.

  • Changed conceptual model: update Concepts.

  • Change that can move a measured figure (a partitioner, an objective weight, the aggregator, a scheduler): rerun python examples/reproduce_readme_figures.py and update the README and Entanglement model from its output. A figure goes into the docs only with a section of that script behind it.

Use Index as the navigation source of truth. If you add a new docs page, link it from the index and from the README documentation section.

Type-checking policy

QuPort ships py.typed; public annotations are part of the developer-facing API. Runtime validation should still guard user-provided values because config files, notebooks, and CLI workflows can pass malformed data even when type checking is not used.

For tests that intentionally pass invalid types to exercise runtime validation, prefer typing.cast(Any, value) at the call site rather than weakening production function signatures.

Runtime validation policy

  • Reject booleans for integer/numeric fields unless a field is explicitly boolean.

  • Validate public API inputs before passing them to Qiskit where possible so users receive QuPort-specific error messages.

  • Keep validation errors deterministic and specific enough for tests.

  • Validate serialized artifacts before writing files when invalid data could leave partial or misleading outputs.

Testing guidance

Add tests at the level where behavior is guaranteed:

  • unit tests for pure helpers and validation;

  • smoke tests for Qiskit-dependent mapping/compilation paths;

  • regression tests for previously observed edge cases;

  • CSV/artifact tests for file-writing behavior.

Prefer small deterministic circuits in tests. Random-circuit tests should always use a fixed seed.

Docs site workflow

The documentation is a Sphinx site built from Markdown with MyST. Source files live in docs/, docs/conf.py owns the production build configuration, and GitHub Actions publishes docs/_build/dirhtml to GitHub Pages from the main branch.

Useful local commands:

make -C docs html      # standard local HTML build
make -C docs strict    # production-equivalent build with warnings as errors
make -C docs linkcheck # external and internal link validation

The repository does not require a dedicated Markdown linter, but contributors should at least check local links when editing docs. A simple check is:

python - <<'PY'
from pathlib import Path
import re, sys
errors = []
for p in [Path('README.md'), *Path('docs').glob('*.md')]:
    for target in re.findall(r'\[[^\]]+\]\(([^)#][^)]*)\)', p.read_text()):
        if '://' in target or target.startswith('mailto:'):
            continue
        local = target.split('#', 1)[0]
        if local and not (p.parent / local).exists():
            errors.append(f'{p}: missing {target}')
print('\n'.join(errors) or 'all local markdown links exist')
if errors:
    sys.exit(1)
PY

Pull request expectations

A strong QuPort PR should include:

  1. a concise motivation;

  2. implementation details;

  3. tests/checks run;

  4. docs updates for user-visible behavior;

  5. notes about limitations or modeling assumptions when applicable.

Validation in hot paths

Public functions validate their arguments on every call. Search loops must not pay for that repeatedly, so several modules pair a validating entry point with a prepared or unvalidated core: quport.network.prepare_routing_tables with route_prepared_link_loads, accumulate_traffic and accumulate_boundary_counts, and quport.hypergraph._ebit_objective_fast.

When changing either half, keep two properties:

  • the prepared path performs the same arithmetic in the same order, so results stay bit-identical rather than merely close;

  • the validating path keeps its checks and its error messages, since those are what the tests pin.

Changing a partitioning objective

The objective terms are not independent knobs. w_port and w_cong were tuned against a w_dist term that charges every cut gate; an e-bit count is smaller by the aggregation factor, so a penalty left at its old scale stops merely biasing the objective and competes with it. That is not hypothetical – before its terms were rescaled, the ebit strategy left 12.6% of the e-bits it is named for above the proved optimum, against 1.6% after (examples/reproduce_readme_figures.py calibration). When adding or reweighting a term, check the terms are commensurate on a real instance, not just that the code runs.

quport.exact is the tool for that check. optimal_partition gives the number a strategy should be compared against, and partition_gap raises outright if a heuristic scores below a proved optimum, since one of the two would then be wrong. It is deliberately independent of the partitioners: a shared bug would have to appear in both a branch-and-bound cost and an annealing objective to go unnoticed. The branch and bound is itself validated against exhaustive enumeration over every feasible assignment in tests/test_exact.py, which is the only real argument that its pruning and canonical form never lose an optimum.