Policies#
These rules bind every change to PowderLine: how versions are numbered, how behavior changes on purpose, which code is frozen, how engine versions move, and how changes are reviewed. They also tell users what a version number promises.
Versioning#
The package follows Semantic Versioning. Every release has an entry in the CHANGELOG.
The recipe schemas are versioned separately from the package. There is one core schema and one engine schema per gateway (
gsasii,topas,easydiffraction), each with a three-part version. A recipe states bothcore_schema_versionandengine_schema_version.Bumping a 0.x schema. A breaking change bumps the minor version (0.1.0 → 0.2.0). Breaking means a recipe that was valid is refused, or means something else. An additive change or a fix bumps the patch (0.1.0 → 0.1.1). Each schema is bumped on its own.
Compatibility is declared, never inferred. Core declares the core schemas it accepts (
SUPPORTED_CORE_SCHEMAS). Each gateway declaresengine_schema_version,supported_engine_schemasandrequires_core_schemaas PEP 440 specifiers. Validation enforces them, andpowderline.support_matrix()lists them.Declarations are exact. Every declaration is written with
==. When a new version has been checked, it is added explicitly. A range is never written in advance.gemmi is pinned exactly (
==0.7.5). Core’s space-group names are gemmi’s canonical names (tests/data/canonical_space_groups.txt), and the symmetry rules are characterized against that version. Moving gemmi means re-running the symmetry tests, regenerating the list and recording the change on Versions.
The current declarations and the schema history are on Versions.
Deliberate behavior changes#
Refinement results are regression references, so a change in behavior is never incidental.
Every intentional change is deliberate. A default, a validation rule, the meaning of a statistic, a unit or a set of files: each change is written down before it merges (what changes, who notices, and why). It is reviewed, and it gets a CHANGELOG entry. Where a user must act, the entry says what to do.
References move only with an equivalence check. The outputs under
examples/**/output/are tracked. To move them, run the old and the new code on the same recipes. Attribute every difference to a listed deliberate change. Only then regenerate the references.Comparisons are tolerance-based. A GSAS-II, scipy or BLAS change can shift refined values at the last digits. The tests compare within a tolerance, with a looser class for correlated size and strain terms, and compare peak lists by row count (Contributing).
Frozen GSAS-II code#
Six modules hold the hand-written functions that set up and run a GSAS-II refinement by manipulating GSAS-II’s project dictionary:
src/powderline/gateways/gsasii/{helpers,project,setters,executors,extractors,constraints}.py
GSAS-II’s project-dictionary conventions can’t be fully pinned down by tests. So these functions change only with the maintainers’ explicit consent.
Behavior-changing edits need the maintainers’ explicit consent before they are made. Propose the edit with its reason and its proof (for example, a diff of the project dictionary before and after).
Comment and docstring edits need no consent: they change no behavior. Don’t leave an outdated comment in place.
New behavior goes into the non-frozen modules around them (
fit_report.py,sites.py,routing.py,paths.py,gateway.py).After any edit, run
pixi run update-code-hashand commitsrc/powderline/_code_hash.jsonwith the change. The hash covers the raw bytes, so a comment edit changes it too.tests/test_code_hash.pyfails until the hash is updated.Replace the GSAS-II server after the edit. A server started earlier still runs the old code. Stop it with
pixi run -e gsasii gsas-server stop, and the next run starts a fresh one.gsas-server restartalso works, but it runs the new server in the foreground of that terminal until you stop it.
Engine-version pins#
Every engine is pinned to one version. The regression references and goldens were produced with it.
Engine |
Pin |
Run-time check |
Suite |
|---|---|---|---|
GSAS-II |
|
|
|
easydiffraction |
|
|
|
TOPAS |
Not installable. A |
|
|
A version outside the specifier raises EngineVersionError.
tests/test_gateways.py checks that the pixi pins lie inside the specifiers,
and tests/test_gateways_installed_engines.py checks the installed engine.
Note
tc.exe carries no version PowderLine can read. So you declare the
installed TOPAS version with --topas-version, the topas_version option of
powderline.run(), or topas.version in .powderline_config.yaml. The
check compares declarations, not the binary.
Try a newer engine#
A pin moves only after a compatibility run. Never widen a pin without one.
scripts/check_engine_version_candidate.py prints the steps for one engine
and candidate, filled in with the current specifier and the files to edit.
It runs no pixi command and edits nothing:
pixi run python scripts/check_engine_version_candidate.py --engine gsasii --version 5.8.0 --rev <commit>
pixi run python scripts/check_engine_version_candidate.py --engine easydiffraction --version 0.22.0
pixi run python scripts/check_engine_version_candidate.py --engine topas --version 7
Make a throwaway environment. In a scratch worktree (
git worktree add ../pl-candidate -b scratch/<engine>-<version>), raise the pin inpixi.tomland widen the gateway’s specifier to admit the candidate. Then runpixi lock,pixi install -e <env>andpixi reinstall powderline. Nothing from this worktree is pushed.Note
Widening the specifier is the test override. The CLI and
powderline.run()have no bypass.allow_unsupported_engine_version=Trueexists only on a gateway’s ownrun(), for tests.Run that engine’s suite,
slowincluded, against the committed references:pixi run -e <env> test-<engine>.Record the result in the pull request: pass or fail per test, and for every regression case the deltas of the refined values, their ESDs and Rwp against the references, even when within tolerance.
On a pass, open one pull request that moves the pin and the specifier together and updates every page that names the engine version. The references are not regenerated.
On a fail, record the behavior change in the same report. Decide between a schema bump (with a re-baseline, see Deliberate behavior changes) and staying pinned. The pin stays where it is until that decision is made.
Per engine:
GSAS-II. The pin is the source revision; the specifier is the run-time
GSASII.__version__. The conda package version comes from the fork’s recipe and is unrelated. A candidate that needs an edit to a frozen module needs the maintainers’ consent first (Frozen GSAS-II code).easydiffraction. A new version also re-runs the settings verification for both calculators (
scripts/verify_easydiffraction_settings.py, about two hours of CPU;--jobs Nruns N processes). It regeneratesspace_group_support.py.tests/test_easydiffraction_space_groups.pyskips untilspace_group_table.pyis regenerated for the installed version (scripts/generate_easydiffraction_space_groups.py). Re-check the behaviors on the easydiffraction page: an upstream fix may retire a workaround.TOPAS. The goldens live under
tests/data/topas/native/golden/<version>/, chosen by each fixture’sengine_version. Adding TOPAS 7 needs a TOPAS 7 installation for thetopas_execrun, its own goldens, and a check of TOPAS 7’s convergence. It also needs every behavior on the TOPAS page marked unverified for TOPAS 7 to be checked on TOPAS 7. The generation suite (topas_gen) runs anywhere and compares INPs byte for byte with the TOPAS 6 goldens, so a change in what is written shows without TOPAS.
Review#
A human is responsible for every change and reviews every line. CI runs the engine-free, GSAS-II and easydiffraction suites on Linux and Windows before merge.
test-all, andtest-topasfor a TOPAS change, are run by hand.A change that moves a reference, a default, a pin or a frozen module is reviewed against its written reason, not against its diff alone.
AI-assisted commits end with one
Assisted-by: <agent>:<model>trailer per model that contributed. An AI is never listed as a co-author.
Ask, don’t assume#
If a design decision is not written down (on this page, on Versions or on an engine page), stop and ask the maintainers. Don’t infer the answer from the code’s current behavior or from what another engine does. Write the answer down where the next contributor will find it.
The reason is the project’s whole stance. An engine’s silent default is not a decision. Plausible inferences about engine quirks have been wrong often enough (origin choices, a variance read as a width, two parameters swapping roles) that the written answer is the only safe one.