This file provides context for AI coding agents working on this project.
These rules apply for every issue and every PR until the project tags v1.0. They override anything in the rest of this file or in the agent's general training that conflicts.
Rename freely. Do not add forwarding aliases, DeprecationWarning
emitters, alias maps, REMOVE-AT-V1.0 markers, or any other
deprecation-cycle infrastructure for any rename, signature change, or
removal. The whole package is still pre-1.0; users have no API
stability contract yet. Make the change cleanly, update every
in-tree call site, document the change in CHANGES.md, move on.
The issue text defines the scope. If something falls under the issue's title or stated motivation, it is in scope by default. Do not unilaterally narrow the scope or split work into "follow-up" issues. If the scope is genuinely ambiguous, ask exactly one clarifying question, then proceed.
Do not file new issues, propose new issues, or build "future v1.0 cleanup" tracking unless the user asks. Pre-1.0 cleanup is just "edit the code now."
When the user asks a brief question, answer briefly: a sentence or a short list, no tables, no preamble, no recap. When the user asks for analysis or a plan, longer is fine. Match the form of the request.
A single clarifying question is cheaper than building the wrong thing and unwinding it. Do not stack multiple questions; ask one, wait, then proceed.
Always sign your work. Every commit message, pull request description, and inline comment authored by an AI agent must end with:
Contributed by: <AgentName> (<model-id>)
Example: Contributed by: OpenCode (argo/claudesonnet46)
-
Before writing any code, if the issue is on one or more project boards, set the issue status to "In progress" on each of those boards:
# Find the project item ID for the issue, then update its status field gh api graphql -f query='...' # see project board queries below
If the issue is on no project board, skip this step — there is no board status to set.
-
Open a PR with a body that includes a closing reference as a bullet, followed by any remarks and the agent/model signature:
- closes #N <optional additional context> Contributed by: OpenCode (argo/claudesonnet46)The
closes #Nkeyword triggers GitHub automation: when the PR is merged the issue is closed and the project card moves to "Done" automatically.Mirror the issue's milestone and boards onto the PR. The PR inherits whatever the issue has — no more, no less:
- If the issue is assigned to a milestone, set the PR to the same milestone. If the issue has no milestone, the PR gets none.
- If the issue is on one or more project boards, add the PR to the same board(s) and set its status to "In Progress". If the issue is on no board, the PR gets no board.
A PR is always created (the no-board / no-milestone case simply means the PR has no board or milestone either) — the absence of a board or milestone is never a reason to skip the PR.
Status field IDs and the corresponding option IDs are project- specific. Discover them per board with the GraphQL query
node(id: "<PROJECT_ID>") { ... fields(first: 20) { ... } }and reuse them where AGENTS.md examples below showSTATUS_FIELD_ID,IN_PROGRESS_OPTION_ID, etc. In Progress is present on every board; "In review" is present only on boards that have been configured with that status — leave the status at "In Progress" on boards where Todo / In Progress / Done are the only options.Match the issue's boards. Before creating the PR, query the issue's
projectItemsto discover every board it belongs to. Add the PR to all of those boards (and only those).Set "In review" when code is complete. Once the PR is pushed, set its project board status to "In review" (on boards that have that status; leave "In Progress" where only Todo / In Progress / Done exist). Skip this when the issue (and therefore the PR) is on no board.
# Set milestone when creating the PR gh pr create --milestone "Example Milestone" ... # Add the PR to the project board immediately after creation PR_NODE=$(gh api repos/OWNER/REPO/pulls/N --jq '.node_id') ITEM_ID=$(gh api graphql -f query="mutation { addProjectV2ItemById(input: {projectId: \"PROJECT_ID\", contentId: \"$PR_NODE\"}) { item { id } } }" --jq '.data.addProjectV2ItemById.item.id') # Set the PR's project board status to "In Progress" # (STATUS_FIELD_ID and IN_PROGRESS_OPTION_ID are the same values used # for the issue status update in step 1 above) gh api graphql -f query="mutation { updateProjectV2ItemFieldValue(input: { projectId: \"PROJECT_ID\" itemId: \"$ITEM_ID\" fieldId: \"STATUS_FIELD_ID\" value: { singleSelectOptionId: \"IN_PROGRESS_OPTION_ID\" } }) { projectV2Item { id } } }"
-
Never close issues manually — let the merge automation do it.
When creating a new project board, set the default view layout to Board (Kanban) so cards are visible by status column. After creation, navigate to the project, open the view settings, and select Board as the layout type. This cannot currently be set via the
ghCLI; it must be done in the GitHub web UI immediately after the project is created. -
Create a feature branch before making any file changes. This is the very first step after reading the issue — create the branch before editing, creating, or deleting any file. All changes must be made on a feature branch, never directly on
main— the only exception is truly trivial changes (e.g. a one-word typo fix in a doc comment). If you have already modified files onmainby mistake, stash the changes, create the branch, then pop the stash. Branch names must start with the issue number followed by a short, hyphen-separated description of the topic:git checkout -b <issue-number>-<concise-topic>Examples:
2-motor-limits,1-wavelength-energy,9-diffraction-modes -
removed
-
Monitor CI after the PR is opened. Watch for test failures, lint errors, or coverage gaps reported by the CI checks. Push additional commits to the feature branch to resolve any failures — do not force- push unless explicitly requested.
-
Resolve PR comments. Address reviewer feedback with further commits on the same branch. Update the PR description if the scope changes materially.
-
Merge once all CI checks are green and the PR is approved (or self-approved if the repository allows it). Use the merge strategy preferred by the repository (typically squash-merge or merge commit — follow the existing pattern in the commit history). The
closes #Nkeyword in the PR body closes the issue and moves the project card to "Done" automatically. -
Local cleanup after a successful merge:
git checkout main git pull --ff-only origin main git branch -d <feature-branch>
Confirm the issue is closed on GitHub and the project card has moved to "Done" before starting the next item.
Confirm before acting on implied direction. If a user's message could be interpreted as either a question or an instruction, ask for clarification before taking action (committing, pushing, opening PRs, etc.).
Contributed by: OpenCode (argo/claudesonnet46)
ad_hoc_diffractometer is a Python package for describing multi-circle
diffractometer geometries used in X-ray and neutron crystallography. It
provides:
- A class-based description of diffractometer stages (rotary axes) and their stacking order
- Demo geometries for standard synchrotron and laboratory diffractometers
- Crystallographic lattice calculations (B matrix, reciprocal lattice)
- Caller-facing axis notation (+x, -z, vertical, transverse, longitudinal)
- A geometry registry (
list_geometries()) for all predefined geometries - Display precision control at package and instance level
diffractometer/ # project root (git repo)
├── AGENTS.md # this file
├── pyproject.toml # package metadata, ruff, isort, pytest config
├── docs/
│ ├── Makefile / make.bat # sphinx build helpers
│ └── source/ # Sphinx documentation source
│ ├── conf.py # Sphinx configuration
│ ├── index.rst # root toctree
│ ├── install.md # installation instructions
│ ├── api.rst # AutoAPI stub
│ ├── changes.md # includes CHANGES.md
│ ├── direct-lattice.md # background material
│ ├── problem1.md / problem2.md / problem3.md
│ ├── problem1_solution.tex / .pdf
│ ├── fourcv_alignment_howto.ipynb
│ └── _static/
│ └── switcher.json # version switcher stub
├── references/ # journal articles and reference documents (excluded from repo)
│ ├── 1967 Busing and Levy a05492.pdf # foundational four-circle paper
│ ├── 1999-JAppl-Cryst-32-614-623-H-You-psic-4S+2D/ # You (1999) psic
│ ├── 1993 J Appl Cryst 26 706 Lohmeier and Vlieg sixc/
│ ├── 2016 RefModuleMatSciMatEng_01215.pdf # Walko — geometry survey
│ └── 2020-12-13-fourcc-alignment-7-id-c/ # real alignment session data
├── src/
│ └── ad_hoc_diffractometer/ # package source
│ ├── __init__.py # public API (Tier 1 names only)
│ ├── geometries/ # declarative YAML demo geometries (#267)
│ │ ├── __init__.py # marker for importlib.resources
│ │ ├── schema.json # JSON Schema for the declarative format
│ │ └── *.yml # one file per declarative demo geometry
│ ├── geometry_loader.py # YAML → AdHocDiffractometer loader (#267)
│ ├── factories.py # geometry registry + shared definitions
│ ├── constants.py # XHAT, YHAT, ZHAT
│ ├── axes.py # parse_axis(), axis_label(), kappa_axis()
│ ├── rotation.py # rotation_matrix() — Rodrigues formula
│ ├── stage.py # Stage class
│ ├── diffractometer.py # AdHocDiffractometer class
│ ├── lattice.py # Lattice class, b_matrix(), standalone fns
│ └── display.py # get/set_precision(), fmt()
└── tests/
├── test_<module>.py # one file per source module (see testing instructions)
├── test_regression_issue_N.py # cross-module regression tests (named by issue)
└── helpers.py / conftest.py # shared test infrastructure
pip install -e . # install in editable mode
python3 -m pre_commit install # install pre-commit hooks (required once)python3 -m pytest # run all tests
python3 -m pytest tests/test_lattice.py # lattice tests only
python3 -m pytest -q # quiet summaryAll tests must pass before committing. Pre-commit runs automatically on
git commit. To run it manually:
python3 -m pre_commit run --all-filesThe repo uses three coordinated mechanisms to keep copyright text consistent. All three are wired into pre-commit and run automatically.
.copyright.txt is the single source of truth for the per-file header
text:
Copyright (c) 2026-2026 UChicago Argonne, LLC
SPDX-License-Identifier: LicenseRef-UChicago-Argonne-LLC-License
The Lucas-C/insert-license pre-commit hook propagates that header to
every covered Python file on each commit. Three hook instances cover:
src/ad_hoc_diffractometer/*.py(excluding_version.py)tests/test_*.pyscripts/*.py
To change the per-file header text project-wide, edit only
.copyright.txt and run pre-commit run --all-files.
A local pre-commit hook (update-copyright-year) runs the
scripts/update_copyright_year.py script on every commit. The script
rewrites the pattern <START>-<OLD_END> → <START>-<CURRENT_YEAR> in
each file listed in its TARGET_FILES:
.copyright.txtLICENSE(line 1 only — the licence body is verbatim per ANL legal and is never edited)docs/source/conf.py
The script exits non-zero when it changes anything, so pre-commit fails and the developer stages the rewrite before retrying the commit. On January 1 of any year, the next commit on any branch will bring every year span forward.
docs/source/conf.py declares copyright as a static year-range string,
not a build-time-dynamic datetime.now().year expression. This is
intentional: the bump script is the single mechanism that keeps
LICENSE, .copyright.txt, and the rendered docs aligned, and a
dynamic expression would hide drift between them.
- The
Copyright (c) YYYY-YYYY ...line on individual.pyfiles — edit.copyright.txtand re-run pre-commit instead. - The end year in
LICENSEline 1,docs/source/conf.py, or.copyright.txton January 1 — the bump script handles it. - The body of
LICENSE— verbatim per ANL legal.
- The start year in any year range (project inception year); the script never touches it.
- Adding new files to
TARGET_FILESif a new file legitimately contains a year range.
The benchmark test suite (tests/test_benchmark.py) includes slow tests
that run forward()/inverse() across all registered geometries and
modes. These are marked @pytest.mark.slow_benchmark and are excluded
by default from pytest via addopts in pyproject.toml.
Fast replacement tests (using a single monkeypatched geometry) maintain
100 % code coverage on benchmark.py during normal development.
When to run full benchmarks locally. If your changes touch any module in the hot path or geometry construction layer, run the full benchmark suite before committing:
python3 -m pytest -m slow_benchmark --no-cov -qHot path: forward.py, kappa.py, mode.py, orientation.py,
rotation.py, reference.py, surface.py
Geometry construction: geometry_loader.py, geometries/*.yml,
factories.py, diffractometer.py, stage.py, axes.py,
constants.py
CI runs these automatically (via .github/workflows/benchmark.yml)
when any of these files change on main or in a pull request.
A clean Sphinx build is necessary but not sufficient to confirm that documentation changes render correctly. Sphinx will emit "build succeeded" even when reStructuredText markup leaks through into the HTML as literal text. Common failure modes:
- A role written inside a Napoleon field-name slot
(e.g.
:attr:`~module.Class.name` : floatin a NumPyAttributessection) is pasted verbatim into the.. py:attribute::directive, so the rendered page shows the raw ``:attr:`~module.Class.name``` text instead of a link. - Inline roles that should produce links (
:func:,:class:,:meth:,:attr:,:doc:,:ref:) silently render as literal text when their target cannot be resolved or when they appear in a context that does not parse RST roles (directive option values, certain table cells, etc.). - MyST/Markdown files containing accidentally-mixed RST syntax produce no warning but display the literal markup.
Required workflow for any change that touches a docstring, a
docs/source/** file, or an AutoAPI Jinja2 template:
- Run
make -C docs clean htmland confirm zero warnings. - Open the affected rendered page(s) under
docs/build/html/(orgrepthe HTML) and visually verify that every cross-reference appears as a real<a>link, not as literal ``:role:`...``` text. - For any class with a NumPy
Attributesdocstring section, alsogrepthe AutoAPI page for:attr:/<span class="pre">:patterns that would indicate role markup leaking into the HTML.
Quick check (from the repository root after rebuilding the docs). Run both patterns — they catch different leak shapes:
# Pattern 1: literal :role:`...` (RST role text passed straight through).
# Catches leaks in .rst files and in MyST contexts where backticks were
# preserved verbatim. Excludes docs/build/html/_modules/ (sphinx.ext.viewcode
# pages display source code verbatim, so role markup there is expected).
grep -rE ':(attr|func|class|meth|mod|doc|ref|obj|data|exc|term):`' \
docs/build/html/ --include='*.html' \
--exclude-dir=_modules \
--exclude-dir=_static \
&& echo "FAIL: literal RST role leak — see above." \
|| echo "OK: no literal RST role leaks."
# Pattern 2: bare :role: prefix that survived even when MyST rewrote the
# backticked content into <code>. This is the failure mode when an RST
# role (e.g. :attr:`~mod.Class.member`) appears in a MyST/Markdown file —
# the MyST parser strips the backticks but leaves the role prefix as
# literal text in front of an inline-code element. Filter out legitimate
# href/title attributes and proper xref/reference link bodies; what
# remains is leaked role-prefix text.
grep -rhE ':(attr|func|class|meth|mod|doc|ref|obj|data|exc|term):' \
docs/build/html/ --include='*.html' \
--exclude-dir=_modules \
--exclude-dir=_static \
| grep -vE 'href=|title=|<title>|class="reference|class="xref' \
| grep -E ':(attr|func|class|meth|mod|doc|ref|obj|data|exc|term):' \
| head -10
# (above prints any leak lines; empty output = OK.)This check belongs alongside the test suite in any pre-PR checklist that involves documentation work.
In MyST/Markdown files use the brace form {role}\`target\``` (for example {attr}`~mod.Class.member```) — never the colon form, which
is silently passed through as literal text.
Several documentation files are generated from source code or other source files and committed to git. When the underlying source changes, these artefacts must be rebuilt and included in the same branch.
Interactive Plotly HTML figures and static SVG fallbacks for all 10
demo geometries live in
docs/source/_static/geometries/<geometry>/<geometry>.{html,svg}.
They are generated by tools/generate_geometry_drawings.py, which
instantiates a
GeometryAxisFigure (from drawing.py) for each registered geometry
and calls write_html() and write_image().
Rebuild command (from repository root):
python tools/generate_geometry_drawings.pyWhen to rebuild: any change to drawing.py, geometries/*.yml,
geometry_loader.py, factories.py, stage.py, or axes.py that
affects stage names, axis labels, physical direction names, basis
dicts, or the drawing layout.
Dependencies: plotly and kaleido (for SVG export). These are
not declared in pyproject.toml runtime deps — install them manually:
pip install plotly kaleidodocs/source/problem1_solution.tex is a standalone LaTeX document
derived from the content of docs/source/problem1.md. The PDF is
committed alongside it.
Rebuild command (from docs/source/):
pdflatex -interaction=nonstopmode problem1_solution.tex
pdflatex -interaction=nonstopmode problem1_solution.tex # second pass
rm -f problem1_solution.{aux,log,out} # clean upWhen to rebuild: any change to problem1_solution.tex. If
problem1.md changes in a way that affects the worked solution, update
the .tex file to match, then rebuild the PDF.
docs/source/howto/fourcv_alignment_howto.ipynb is an authored notebook
(not auto-generated). It contains markdown cells describing the fourcv
geometry. Edit it directly when terminology, axis labels, or geometry
descriptions change. Sphinx executes it during make html via
myst-nb, so its code cells must remain runnable.
ad_hoc_diffractometer is a pure-Python package. The runtime
dependencies beyond the Python Standard Library are NumPy (numerical
arrays and linear algebra) and PyYAML (parser for the declarative
geometry files in src/ad_hoc_diffractometer/geometries/; see issue
#267). There is no dependency on scipy, sympy, or any other scientific
library. Keep it that way: every new algorithm must be implementable
with NumPy alone.
- Import alias:
import ad_hoc_diffractometer as ahd - Demo geometries:
g = ahd.make_geometry("fourcv")— the demo geometries are declarative YAML files underad_hoc_diffractometer/geometries/, registered automatically by the loader at import time and discoverable viaad_hoc_diffractometer.list_geometries() - Tier 1 (top-level): Only ~23 names are exported from
__init__.py(core classes, orientation, modes, registry). All other names are accessed via their submodule:ahd.display.fmt(),ahd.radiation.energy_to_wavelength(),ahd.conversions.hkl_to_d(), etc. - Python: ≥ 3.10 (uses
X | Yunion types) - Dependencies:
numpyandpyyamlonly; no scipy, no sympy - Style: ruff (E, W, F, UP, B rules) + ruff-format, line length 88;
E501ignored (slight overruns tolerated) - Import sorting: isort with
force_single_line = true - No
geometry_prefix on demo-geometry function names display.fmt(value, digits)for all floating-point display; never use f-strings with hardcoded precision for user-facing output- US English spellings — see the dedicated section US English spelling (mandatory) below.
Every word you write — in code, comments, docstrings, error messages, log messages, exception text, type names, identifiers, filenames, branch names, commit messages, PR descriptions, issue comments, YAML strings, JSON descriptions, and Markdown / reST documentation — must use US (American) English spelling. This rule is non-negotiable and is checked at review time.
This applies even when paraphrasing or summarizing material that uses British spellings (e.g. literature references): retype the prose into US English when incorporating it into the project. Verbatim quotations inside quotation marks may preserve the source's spelling; everything else must conform.
| Use (US) | NOT (British) |
|---|---|
analyze, analyzer, analyzed, analyzing |
analyse, analyser, analysed, analysing |
behavior, behaviors |
behaviour, behaviours |
center, centers, centered, centering |
centre, centres, centred, centring |
color, colors, colored, coloring |
colour, colours, coloured, colouring |
characterize, characterized, characterizing |
characterise, characterised, characterising |
customize, customized |
customise, customised |
defense, defensive |
defence, defencive |
emphasize, emphasized |
emphasise, emphasised |
factorize, factorized |
factorise, factorised |
favor, favorite |
favour, favourite |
fiber, fibers |
fibre, fibres |
fulfill, fulfilled |
fulfil, fulfilled (British single-l) |
generalize, generalized, generalizing |
generalise, generalised, generalising |
gray |
grey |
honor, honors, honored, honoring |
honour, honours, honoured, honouring |
initialize, initialized, initializing |
initialise, initialised, initialising |
labeled, labeling |
labelled, labelling |
liter, liters |
litre, litres |
maximize, maximized |
maximise, maximised |
meter, meters (the unit) |
metre, metres |
millimeter, millimeters |
millimetre, millimetres |
minimize, minimized, minimizing |
minimise, minimised, minimising |
modeled, modeling |
modelled, modelling |
normalize, normalized, normalizing |
normalise, normalised, normalising |
optimize, optimized, optimizing |
optimise, optimised, optimising |
organization |
organisation |
parameterize, parameterized |
parameterise, parameterised |
polarization, polarized, polarizer |
polarisation, polarised, polariser |
practice (verb) |
practise |
program |
programme (in software contexts) |
realize, realized |
realise, realised |
recognize, recognized, recognizing |
recognise, recognised, recognising |
serialize, serialized, serializing |
serialise, serialised, serialising |
signaled, signaling |
signalled, signalling |
specialized, specializing |
specialised, specialising |
stabilize, stabilized |
stabilise, stabilised |
summarize, summarized |
summarise, summarised |
synthesize, synthesized |
synthesise, synthesised |
theater |
theatre |
traveled, traveling, traveler |
travelled, travelling, traveller |
utilize, utilized |
utilise, utilised |
visualize, visualized, visualizing |
visualise, visualised, visualising |
The list above is illustrative, not exhaustive. Whenever you are
unsure, prefer the spelling used by the Merriam-Webster dictionary
or the OED's "US" tag. The rule applies to every variant of the
listed roots (-ize / -ization / -izing / -ized etc.).
Before opening a pull request — and again after addressing reviewer
comments — run this check on every file you have added or modified
(replace $FILES with git diff --name-only main...HEAD or an
equivalent list, excluding AGENTS.md itself because the
substitution table above intentionally contains British spellings as
negative examples):
grep -EnH "\b(honour|honours|honoured|honouring|colour|colours|coloured|colouring|behaviour|behaviours|analyse|analysed|analyser|analysers|analyses|analysing|polariser|polarisers|polarisation|polarised|polarising|normalise|normalised|normalises|normalising|minimise|minimised|minimises|minimising|maximise|maximised|maximises|maximising|optimise|optimised|optimises|optimising|generalise|generalised|generalises|generalising|recognise|recognised|recognises|recognising|characteris|millimetre|centre|centres|centred|centring|customise|customised|emphasise|emphasised|factorise|factorised|fibre|fibres|favour|favourite|grey|initialise|initialised|labelled|labelling|litre|litres|metre|metres|modelled|modelling|organisation|parameterise|parameterised|realise|realised|serialise|serialised|signalled|signalling|specialised|specialising|stabilise|stabilised|summarise|summarised|synthesise|synthesised|theatre|travelled|travelling|traveller|utilise|utilised|visualise|visualised|visualising|programme)\b" $FILESThe check must produce no output (other than expected hits inside
the AGENTS.md substitution table, which should be excluded from
$FILES). Any hit elsewhere must be corrected before review. This
check belongs alongside pytest, pre-commit, and the documentation
grep checks in your pre-PR checklist.
A small number of British spellings remain in pre-existing files that were authored before this rule was tightened. Do not silently fix them in unrelated PRs — open a dedicated cleanup PR (or include them explicitly in a PR whose scope already touches those files) so the diff is reviewable.
CHANGES.md is a user-facing changelog, not a design document. Each
bullet is a few words plus the issue/PR reference. The issue/PR
reference carries the full load of explanation — readers who want
details follow the link. The bullet itself names the change in the
fewest words that identify it.
Hard length cap: ≤ 12 words before the (#NNN) reference. If
you find yourself writing a second clause, a semicolon, an "and", a
parenthetical aside, or a "users must …" instruction in the bullet
itself, the bullet is too long — cut it down. All of that material
belongs in the referenced issue or PR, not in the changelog.
Use v0.6.0 and earlier as the style reference. Several recent
entries (v0.7.0, v0.8.0) drifted toward multi-sentence bullets with
implementation detail and benchmark numbers; do not imitate them.
v0.6.0 is the canonical pattern: each bullet is a short headline
followed by (#NNN) or (#NNN, #MMM).
Examples of acceptable length:
- Rotation composition and `ub_identity` corrected. (#280)
- B matrix for non-cubic lattices now matches BL1967. (#280)
- Doc version-switcher dropdown now shows every published version. (#273)
- Switch to declarative YAML for demo geometries. (#267)
Examples of bullets that are too long and must be cut:
- Compose stage rotations outermost-leftmost (BL1967); place crystal a*
along the beam at ub_identity; saved UB matrices must be re-derived. (#280)
- Default UB matrix (ub_identity) now places the crystal a-axis along
the beam direction; sessions relying on the previous default must
explicitly set U or measure orienting reflections. (#NNN)
Both of the "too long" examples pack the issue body into the changelog. The issue/PR already has the body; the bullet should not duplicate it.
- One bullet per change. A few words followed by the issue/PR reference. No second sentence, no rationale, no cross-validation numbers, no comparison to other packages, no internal implementation detail, no benchmark numbers, no migration instructions.
- Use the existing subsection headings:
Breaking changes,Behavior change,Added,Changed,Fixed. Omit any that are empty. - Sort bullets alphabetically within each
###subsection. Case-insensitive; treat surrounding backticks, asterisks, and punctuation as transparent so the leading word is what sorts. This makes diff review predictable and keeps related bullets near each other across releases. Applies going forward (post-v0.11.1); do not silently re-sort historical release sections in unrelated PRs — open a dedicated cleanup PR if you want to backfill. - A correctness fix that changes computed numerical output gets one
short
Behavior changebullet naming what changed. Migration instructions (e.g. "re-derive saved UB matrices") belong in the referenced issue/PR, not in the bullet. - Do not add bullets for test-suite changes (new tests,
parametrization edits, test-only HKL substitutions), docstring
fixes, or internal refactoring. A
Fixedentry implicitly includes its regression test. - The release header is
## Release vX.Y.Zfollowed byReleased YYYY-MM-DD.and nothing else. No prose paragraph. - A single-fix release should be three or four lines of bullets, not more (see v0.9.0 for the model: two bullets, one line each).
When in doubt, err on the side of fewer words. If a reviewer can look at the bullet and identify what the change is about (well enough to decide whether to click the issue link), it is long enough.
Contributed by: OpenCode (argo/claudeopus47)
Test file naming
- Each test file
tests/test_<module>.pycorresponds to exactly one source modulesrc/ad_hoc_diffractometer/<module>.py. Keep this one-to-one mapping when adding new modules. - Do not create
test_<feature>.pyfiles that cover content from an existing module — add the tests to the matchingtest_<module>.py. - Exception: purpose-specific files such as
test_regression_issue_N.pyare permitted for cross-module bug reports added later in the project's life, where the fix spans multiple modules and there is no single natural home. These files should say so in their module docstring.
Test content
- Every parametrized test set uses
pytest.param(..., id="descriptive-id") - Every parametrized set includes a
contextparameter that is eitherdoes_not_raise()(fromcontextlib.nullcontext) orpytest.raises(ExcType, match=re.escape("expected text")) - The
matchstring is embedded directly inpytest.raises(...)— it is not passed as a separate parameter to the test function - All test code runs inside
with context:block - Add tests for every new function, class, and error case
- Run
python3 -m pytest -qafter any change and fix all failures before committing
The default coordinate system follows You (1999):
| Basis vector | Physical direction |
|---|---|
XHAT (+x) |
vertical (out of floor) |
YHAT (+y) |
longitudinal (along beam, toward equipment) |
ZHAT (+z) |
transverse (to our left facing the equipment) |
The Busing & Levy (1967) convention (used in fourcv, fourch,
kappa4cv, kappa4ch) has x=transverse, y=longitudinal, z=vertical.
Basis dicts (BASIS_YOU, BASIS_BL) in factories.py encode these.
A third constant, BASIS_DEFAULT (vertical=YHAT, longitudinal=ZHAT,
transverse=XHAT), is the neutral fallback used by the declarative
geometry loader when a YAML file omits the basis: key. It is
deliberately distinct from BASIS_YOU and BASIS_BL so that the
package does not appear to espouse either literature convention as a
"project default."
The sign of a stage's axis vector encodes its handedness:
+nHat→ right-handed rotation about nHat (thumb along +nHat, fingers curl positive)-nHat→ left-handed rotation about nHat (equivalent to right-handed with negated angle)
This is the parse_axis() / kappa_axis() / axis_label() system in
axes.py. Stage internal attributes always store a numpy array; the
string notation ("+x", "-z", "vertical", "-transverse") is the
caller-facing interface only.
All demo geometries are declarative YAML files in
src/ad_hoc_diffractometer/geometries/, parsed at import time by
geometry_loader.py and registered in the geometry registry; they
appear in list_geometries(). Access them as
ahd.make_geometry("fourcv"), etc.
Naming convention:
| Suffix | Meaning |
|---|---|
v |
vertical scattering plane (synchrotron) — ttheta rotates about the transverse axis |
h |
horizontal scattering plane (laboratory) — ttheta rotates about the vertical axis |
| no suffix | detector convention unambiguous (psic, sixc) or compound (zaxis, s2d2) |
Current demo geometries: psic, fourcv, fourch, sixc, kappa4cv,
kappa4ch, kappa6c, zaxis, s2d2, fivec.
Walko (2016) designation system is noted in docstrings (S3D1, S4D2, etc.).
The kappa axis is:
kappa_axis = vertical * cos(alpha) + transverse * sin(alpha)where alpha is measured from the vertical axis toward the transverse axis
(confirmed by ITC Vol. C Sec. 2.2.6 and Walko 2016). Default alpha = 50°.
Lattice deduces the crystal system from the minimum number of supplied
parameters. The 7 systems and their minimum inputs:
| System | Minimum |
|---|---|
| cubic | a |
| tetragonal | a, c |
| hexagonal | a, c, gamma=120 (gamma must be explicit) |
| trigonal | a, alpha (alpha ≠ 90) |
| orthorhombic | a, b, c |
| monoclinic | a, b, c, beta |
| triclinic | a, b, c, alpha, beta, gamma |
Properties cartesian_lattice_vectors, reciprocal_lattice_vectors, and
B are lazy-computed and cached. Any parameter setter invalidates the
cache and re-deduces the crystal system.
The B matrix follows the I16 convention: (b1, b2, b3) = 2π * B.T.
| Citation | What it defines |
|---|---|
| Busing & Levy, Acta Cryst. 22, 457-464 (1967) | Four-circle geometry, B matrix, U matrix, UB matrix |
| Bloch, J. Appl. Cryst. 18, 33-36 (1985) | Z-axis geometry |
| Vlieg et al., J. Appl. Cryst. 20, 330-337 (1987) | Five-circle geometry |
| Lohmeier & Vlieg, J. Appl. Cryst. 26, 706-716 (1993) | sixc surface geometry |
| Evans-Lutterodt & Tang, J. Appl. Cryst. 28, 318-326 (1995) | S2D2 geometry |
| You, J. Appl. Cryst. 32, 614-623 (1999) DOI:10.1107/S0021889899001223 | psic 4S+2D six-circle; axis sign conventions (mixed handedness) |
| ITC Vol. C, Sec. 2.2.6 (2006) DOI:10.1107/97809553602060000577 | Kappa 50° convention; normal-beam equatorial geometry |
| Walko, Ref. Module Mater. Sci. Mater. Eng. (2016) | Geometry survey; S/D designation system; kappa, zaxis, s2d2 |