Config And Manifests

The experiment configuration schema. Pydantic validates the config at the boundary — no Hydra, no DictConfig reaches the core. Every field has a sensible default, so an experiment.yaml only specifies input references and overrides.

Pydantic configuration schema for a diffBloch experiment.

Config is validated at the boundary: no Hydra, and no DictConfig reaches the core. Every field carries a sensible default (“defaults as code”), so an experiment.yaml only needs to specify input references and overrides.

class diffBloch.config.schema.BlochwaveConfig(*, solver: SolverMethod = 'matrix_exp', absorption: bool = False, rsg: float = 0.66, dsg: float = 0.0015, rocking_curve_sampling: int = 42, mosaicity: bool = False, fixed_n_segments: int = 12, coupling_mode: Literal['union', 'per_tilt'] = 'union', g_max: float = 2.25, sg_max: float = 0.01, union_adaptive: bool = True, union_max_new_beams_pct: float = 0.01, ignore_orientations: tuple[int, ...] = ())[source][source]

Bases: _StrictConfig

The beam-selection, coupling, and dynamical-solver settings for the Bloch-wave simulation.

The structure-factor support grid is not a config field: it is derived as 2x the solve cutoff (g_max), because a beam set bounded by |g| <= cutoff produces F(g - h) terms reaching 2 * cutoff – so declaring both cutoff and support would let them contradict (one beam cutoff, support derived). The orientation-independent seed pool (from_experiment’s difference-safe candidate set) and the scored-reflection cap (optimize_orientation’s trial-coupling window) both read g_max directly, rather than a separate smaller radius. rsg / dsg are the Klar beam-selection cutoffs and rocking_curve_sampling the tilt count. The shared integration semi-angle is read from the PETS experimental data rather than configured. mosaicity: true opts into PETS-derived angular mosaic averaging, while the default false disables it.

solver: SolverMethod
absorption: bool
rsg: float
dsg: float
rocking_curve_sampling: int
mosaicity: bool
fixed_n_segments: int
coupling_mode: Literal['union', 'per_tilt']
g_max: float
sg_max: float
union_adaptive: bool
union_max_new_beams_pct: float
ignore_orientations: tuple[int, ...]
to_absorption() Absorption[source][source]

Parse the absorption switch into its typed scientific value.

to_beam_selection(integration: IntegrationGeometry) BeamSelection[source][source]

Assemble the select_beams value-type: the Klar cutoffs + the shared integration.

to_rocking_curve(integration: IntegrationGeometry) RockingCurve[source][source]

Assemble the integrate_rocking_curve value-type: tilt count + the shared integration.

The caller passes the PETS-derived value to both this method and to_beam_selection(), so the beam window and tilt sweep cannot disagree.

to_policy() UnionCoupling | PerTiltCoupling[source][source]

Assemble the selected tilt-dependent Bloch-wave beam-coupling policy.

to_orientation_selection() OrientationSelection[source][source]

Parse zero-based source PETS indices excluded from the whole Bloch experiment.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.SampleConfig(*, thicknesses: tuple[float, ...]=(820.0, ), mean_thickness_by_dataset: dict[str, float]=<factory>)[source][source]

Bases: _StrictConfig

Fixed sample properties.

Thickness is captured here because it is a sample/nuisance parameter, not a numerical-accuracy knob. A later refinement stage can make it refinable without splitting its config home.

thicknesses: tuple[float, ...]
mean_thickness_by_dataset: dict[str, float]
seed_thicknesses_for(exp_data: str) tuple[float, ...][source][source]

Return this dataset’s configured seed thickness tuple.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.DataSplitConfig(*, train_test: bool = False, val_frac: float = 0.2)[source][source]

Bases: _StrictConfig

Train/validation split declaration.

train_test=False (default) trains on every rotation – no held-out validation set, matching the behaviour of an experiment.yaml that never mentions split. train_test=True excludes an evenly spaced val_frac fraction of rotations from the refinement objective (preprocessing still fits their orientation/thickness) and reports their held-out wR2/R_obs alongside the training-set numbers – opt in per experiment, since it means training sees less data.

train_test: bool
val_frac: float
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.LossMetricsConfig(*, residual: Literal['wr2', 'robs'] = 'wr2')[source][source]

Bases: _StrictConfig

The one residual driving the whole pipeline: preprocess search AND refinement.

residual parses into a matching LossFn/ScoresFn pair (to_loss() / to_scores()) – the scalar the gradient refinement minimises and the per-thickness vector optimize_orientation/optimize_thickness search, off the same metric (see diffBloch.engine.losses). A top-level ExperimentConfig field (not scoped under refinement) because it governs preprocessing too, not just the gradient stage; only implemented terms are admissible. Only knobs the default path actually consumes live here – outlier rejection, penalty/nuisance weighting, and gradient-norm reporting are not accepted config keys until a consumer reads them, rather than accepted-but-ignored (cf. penalties, which are Python/API composition, not config).

residual: Literal['wr2', 'robs']
to_loss() LossFn[source][source]

Parse the residual into the scalar LossFn the gradient refinement minimises.

to_scores() ScoresFn[source][source]

Parse the residual into the per-thickness ScoresFn the preprocessing search uses.

The exact per-thickness form to_loss() sums to a scalar – see diffBloch.engine.losses – so the two always agree on one metric.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.OptimizerConfig(*, name: Literal['lbfgs', 'adam', 'adamw'] = 'adam', lr: float = 0.001)[source][source]

Bases: _StrictConfig

Explicit optimizer backend for a refinement stage (matches OptimizerName).

name: Literal['lbfgs', 'adam', 'adamw']
lr: float
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.TrainableConfig(*, positions: Literal['all', 'none'] = 'all', adp: Literal['all', 'none'] = 'all', occupancy: Literal['all', 'none'] = 'none')[source][source]

Bases: _StrictConfig

Whole-group trainable selections for a refinement stage.

A 1:1 edge over TrainableSpec: each group is all or none and parses (via to_spec()) into an AtomSelection. Element-filtered selections (e.g. freeze H) are not config: they are Python/API composition (see with_hydrogen_riding()).

positions: Literal['all', 'none']
adp: Literal['all', 'none']
occupancy: Literal['all', 'none']
to_spec() TrainableSpec[source][source]

Parse into the TrainableSpec the refinement optimizer consumes.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.ThicknessNNConfig(*, enabled: bool = True, num_samples: int = 40, sample_thickness: bool = False, form: Literal['min_thickness'] = 'min_thickness', min_thickness: float = 100.0, max_thickness: float = 2000.0, init_seed: int = 0)[source][source]

Bases: _StrictConfig

Recorded apparent-thickness neural network used by the default refinement path.

enabled: bool
num_samples: int
sample_thickness: bool
form: Literal['min_thickness']
min_thickness: float
max_thickness: float
init_seed: int
to_spec() ApparentThicknessNetwork[source][source]

Parse the YAML block into its validated value-type.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.RefinementConfig(*, steps: int = 40, trainable: TrainableConfig = <factory>, optimizer: OptimizerConfig = <factory>, split: DataSplitConfig = <factory>, thickness_nn: ThicknessNNConfig = <factory>)[source][source]

Bases: _StrictConfig

Stable execution knobs for the default single-stage app refinement (run refine).

These tune the default path; they do not author a scientific program. Scientific composition (hard constraints such as hydrogen riding, soft penalties, freeze-H masks, multi-stage workflows) is expressed as typed Python/API values – see build_refinement_model(), build_refinement_problem(), and with_hydrogen_riding() – and is promoted to config only once the default recipe commits to it as stable public behaviour.

steps: int
trainable: TrainableConfig
optimizer: OptimizerConfig
split: DataSplitConfig
thickness_nn: ThicknessNNConfig
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.NelderMeadOptimizationConfig(*, step_size: float = 0.05, max_iterations: int = 60, x_tolerance: float = 0.001, f_tolerance: float = 0.001, penalize_fewer_reflections: bool = True)[source][source]

Bases: _StrictConfig

Bounds for the optimize_orientation local Nelder-Mead search (preprocess).

The YAML edge: parses (via to_search()) into the validated NelderMeadSearch value-type.

step_size: float
max_iterations: int
x_tolerance: float
f_tolerance: float
penalize_fewer_reflections: bool

Parse into the validated value-type the pure optimize_orientation consumes.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.OrientationOptimizationConfig(*, nelder_mead: NelderMeadOptimizationConfig = <factory>)[source][source]

Bases: _StrictConfig

Bounds for the optimize_orientation orientation search (preprocess).

The YAML edge: parses (via to_search()) into the validated NelderMeadSearch value-type the pure optimize_orientation consumes (the nelder_mead block), and delegates all validation there (one rule home, no drift).

nelder_mead: NelderMeadOptimizationConfig

Parse into the validated value-type the pure optimize_orientation consumes.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.ThicknessOptimizationConfig(*, min_thickness: float = 5.0, max_thickness: float = 2000.0, n_steps: int = 100, plot: bool = False)[source][source]

Bases: _StrictConfig

Bounds for the optimize_thickness per-rotation grid search (preprocess).

The YAML edge: parses (via to_grid()) into the validated ThicknessGrid value-type the pure optimize_thickness consumes, and delegates all validation there (one rule home, no drift). Defaults derive from that value-type (_THICKNESS_GRID_DEFAULTS), so the boundary value cannot drift from it either.

plot is reporting-only – it selects whether the CLI attaches a ThicknessPlotLogger (one wR2-vs-thickness PNG per rotation, default <inputs.structure's directory>/thickness_optim); it never changes the fitted Plan, so dataset_config_digest() excludes it explicitly even when the rest of this block is in scope.

min_thickness: float
max_thickness: float
n_steps: int
plot: bool
to_grid() ThicknessGrid[source][source]

Parse into the validated value-type the pure optimize_thickness consumes.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.PreprocessConfig(*, optimize_orientation: bool = True, optimize_thickness: bool = True, stage_order: Literal['orientation_first', 'thickness_first']='thickness_first', orientation: OrientationOptimizationConfig = <factory>, thickness: ThicknessOptimizationConfig = <factory>)[source][source]

Bases: _StrictConfig

Preprocess-stage configuration (the Plan -> Plan calibration pipeline).

Grouping, not composition: each block configures one preprocess step. Only steps the default run composes get a config block here: optimize_orientation under orientation and optimize_thickness under thickness. The optional converge_numerics driver is not in the default recipe, so it has no config block – a caller that composes it constructs ConvergenceTest / ConvergenceTolerance at the composition site (which carry their own defaults). Opt-in step config lives with the step, not in an always-present block.

optimize_orientation: bool
optimize_thickness: bool
stage_order: Literal['orientation_first', 'thickness_first']
orientation: OrientationOptimizationConfig
thickness: ThicknessOptimizationConfig
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

diffBloch.config.schema.dataset_checkpoint_stem(ref: str) str[source][source]

The per-dataset checkpoint name stem for an exp_data ref: plan.<stem>.npz.

Path separators become __ and a .cif_pets suffix is dropped, e.g. undamaged/frame_1.cif_pets -> undamaged__frame_1. Checkpoint identity follows the file, not its position in exp_data, so reordering or inserting datasets never restales another dataset’s checkpoint. Two refs may sanitize to the same stem (a/b vs a__b); Inputs rejects such configs up front rather than letting two datasets share one checkpoint on disk.

class diffBloch.config.schema.Inputs(*, structure: str, exp_data: str | list[str], multi_dataset: bool = False, load_hydrogens: bool = False, isotropic_displacements_only: bool = False)[source][source]

Bases: _StrictConfig

Input references — relative to the experiment directory only (no project-root paths).

structure: str
exp_data: str | list[str]
multi_dataset: bool
load_hydrogens: bool
isotropic_displacements_only: bool
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.schema.ExperimentConfig(*, name: str, inputs: Inputs, sample: SampleConfig = <factory>, blochwave: BlochwaveConfig = <factory>, preprocess: PreprocessConfig = <factory>, loss_metrics: LossMetricsConfig = <factory>, refinement: RefinementConfig = <factory>)[source][source]

Bases: _StrictConfig

A whole experiment, validated at load. No Hydra, no DictConfig.

name: str
inputs: Inputs
sample: SampleConfig
blochwave: BlochwaveConfig
preprocess: PreprocessConfig
loss_metrics: LossMetricsConfig
refinement: RefinementConfig
to_declaration(integrations: Sequence[IntegrationGeometry]) ExperimentDeclared[source][source]

Project the result-determining knobs onto the run’s declaration event.

One more to_* edge alongside BlochwaveConfig.to_policy() / to_absorption(): the config already owns every value here, so mapping them lives with it rather than in whichever caller happens to emit the event. Any backend (W&B/Comet hyperparameters, the written summary) reads the run’s settings from this one event instead of being handed the config object. integrations is one PETS-derived geometry per inputs.exp_data entry, in that order.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

diffBloch.config.schema.load_config(path: str | Path) ExperimentConfig[source][source]

Parse and validate one experiment.yaml.

Fails fast with a pydantic.ValidationError at the boundary, rather than a deferred runtime surprise deep in the pipeline.

Experiment locks hash input bytes only; the preprocess and refinement locks hash generated artifacts.

Experiment lock and checkpoint-lock helpers.

experiment.lock identifies input bytes only; the preprocess and refinement locks identify generated artifacts and execution identity. Keeping those separate avoids circular provenance and keeps cache keys stable.

type diffBloch.config.manifest.CellParameters = tuple[float, float, float, float, float, float]
class diffBloch.config.manifest.InputLock(*, ref: str, sha256: str, bytes: int)[source][source]

Bases: BaseModel

Hash and size for one input reference.

ref: str
sha256: str
bytes: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.manifest.ExperimentLock(*, structure: InputLock, experimental_data: InputLock | list[InputLock])[source][source]

Bases: BaseModel

experiment.lock: exact input identity, never generated outputs.

experimental_data is one lock for a single dataset or a list in inputs.exp_data order for a pooled (inputs.multi_dataset) experiment.

structure: InputLock
experimental_data: InputLock | list[InputLock]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.manifest.ArtifactHash(*, path: str, sha256: str, bytes: int, media_type: str)[source][source]

Bases: BaseModel

Hash and media metadata for a generated run artifact.

path: str
sha256: str
bytes: int
media_type: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.manifest.RecipeStep(*, name: str, params: dict[str, Any] | None = None)[source][source]

Bases: BaseModel

One step’s identity in a preprocess recipe: its name + serialized params (or None).

Mirrors StepRecord as plain, comparable data – the lock stores the recipe as a readable list of these, decoupled from the preprocess step vocabulary.

name: str
params: dict[str, Any] | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.manifest.PreprocessLock(*, structure: InputLock, experimental_data: InputLock, authoritative_cell: CellParameters, ignored_rotations: tuple[int, ...], config_digest: str, code_version: str, recipe: list[RecipeStep], plan: ArtifactHash)[source][source]

Bases: BaseModel

plan.<stem>.lock: binds one dataset’s Plan checkpoint to everything that determined it.

A checkpoint is safe to reuse only when the current run matches on every axis – the structure and this dataset’s input bytes, the authoritative PETS cell shared by the experiment, the dataset-scoped config projection (dataset_config_digest()), this dataset’s file-local ignored rotations, the software version, and the composed recipe – AND the .npz verifies against plan. The identity is deliberately per dataset: nothing here hashes the whole experiment.lock or the full inputs.exp_data list, so adding, removing, or reordering other datasets in a pooled experiment never restales this one’s checkpoint unless the first dataset’s authoritative cell changes. The recipe axis distinguishes checkpoints built from the same inputs and config by different step sequences; code_version is the software-implementation axis the recipe (step shape + params) cannot capture. The full code_version string (__version__+g<sha>[.dirty]) is recorded here as a build stamp, but the reuse gate compares only its release __version__ (see preprocess_lock_status()), so the checkpoint survives commits within a release. Identity only: hashes + a readable recipe, never payload.

structure: InputLock
experimental_data: InputLock
authoritative_cell: CellParameters
ignored_rotations: tuple[int, ...]
config_digest: str
code_version: str
recipe: list[RecipeStep]
plan: ArtifactHash
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class diffBloch.config.manifest.RefinementLock(*, plan_lock_sha256s: list[str], refinement_config_digest: str, code_version: str, refined_structure: ArtifactHash, refined_parameters: ArtifactHash)[source][source]

Bases: BaseModel

refinement.lock: binds refined-structure outputs to everything that produced them.

The refinement-stage counterpart to PreprocessLock. Refinement runs on top of already-settled per-dataset Plans, so everything that determines those – inputs, sample, blochwave, preprocess config, recipe – is already pinned by plan_lock_sha256s (the hashes of the exact plan.<stem>.lock files this run refined from, in inputs.exp_data order). What this lock adds is what refinement itself contributes on top: the refinement-determining config (refinement_config_digest(), which includes the train/val split – the split partitions rotations at refinement time and no longer shapes the checkpointed plans) and the code version that ran it, plus hashes of the refined outputs. Verifiable independently of whether the plan locks are still present or match – plan_lock_sha256s is a recorded fact about that run, not a live re-check.

plan_lock_sha256s: list[str]
refinement_config_digest: str
code_version: str
refined_structure: ArtifactHash
refined_parameters: ArtifactHash
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

diffBloch.config.manifest.sha256_file(path: str | Path) str[source][source]

Return the SHA256 hex digest for path.

diffBloch.config.manifest.input_lock_for(path: str | Path, *, ref: str) InputLock[source][source]

Build an InputLock for an experiment input file.

diffBloch.config.manifest.artifact_hash_for(path: str | Path, *, root: str | Path) ArtifactHash[source][source]

Build an ArtifactHash for a generated run artifact.

diffBloch.config.manifest.load_experiment(directory: str | Path) tuple[ExperimentConfig, ExperimentLock][source][source]

Load experiment.yaml, verifying experiment.lock (in reproducibility/) against input bytes, creating that lock first from the current input bytes if it doesn’t exist yet.

First-run convenience: a brand-new experiment directory has no lock to verify against, so there is nothing to protect by refusing to proceed. The lock is created here instead, exactly as write_experiment_lock() would. An existing lock that no longer matches the input bytes still raises (see _verify_input()): that mismatch is the drift this file exists to catch, and silently rewriting it on every run would defeat the purpose. Delete the lock and rerun, or use diffbloch lock-experiment --force (or write_experiment_lock(..., force=True)), to update it after an intentional input change.

diffBloch.config.manifest.write_experiment_lock(directory: str | Path, *, force: bool = False) ExperimentLock[source][source]

Hash inputs.structure and every inputs.exp_data file and write reproducibility/experiment.lock (creating that directory if needed).

The diffbloch lock-experiment CLI command’s implementation. By default this creates a lock only when none exists yet: an existing lock is the experiment’s accepted input baseline, so replacing it requires force=True. Use force=True only after an intentional input change. With unchanged inputs, a forced rewrite reproduces the lock byte-for-byte. If the input bytes changed, replacing the experiment lock invalidates existing plan and refinement locks.

diffBloch.config.manifest.dataset_config_digest(config: ExperimentConfig, *, exp_data: str) str[source][source]

SHA256 of the config that determines one dataset’s settled Plan – its lock identity.

Keyed on the resolved ExperimentConfig (not the experiment.yaml bytes): stable under comment/whitespace/field-order edits, sensitive to any validated-value change in scope. sort_keys makes it order-independent.

Scope is an explicit projection onto exactly what determines the settled per-dataset Plan – so a committed checkpoint is restaled only by a change that could alter it:

  • inputs – rewritten to this dataset’s view: exp_data is the single exp_data ref the checkpoint belongs to (never the full list – other datasets joining or leaving the pool cannot alter this one’s plan), and multi_dataset is dropped for the same reason (a dataset’s settled plan is independent of whether it is pooled);

  • sample, blochwave – shape the grid and beams; blochwave.ignore_orientations is dropped here because it indexes the pooled rotation space – the translated file-local slice lives explicitly in PreprocessLock.ignored_rotations instead, so an ignore edit restales exactly the datasets it lands on;

  • preprocess – shapes and configures the fitting steps, but orientation/thickness only when the matching optimize_orientation/optimize_thickness flag enables that step (the step’s own params already ride in the recipe axis whenever it actually runs – see as_step() – so including them here unconditionally would restale a checkpoint over config that provably never touched it), and always excluding thickness.plot (reporting-only, never touches the Plan even when thickness is in scope);

  • loss_metrics – the residual optimize_orientation/optimize_thickness search minimises (to_scores()).

Everything else is excluded because it cannot change the Plan: name (a label), and all of refinement – including split, which partitions rotations at refinement time and no longer shapes the checkpointed plan (it rides in refinement_config_digest()). This is the config axis of the per-dataset preprocess lock only, not a whole-config identity.

diffBloch.config.manifest.refinement_config_digest(config: ExperimentConfig) str[source][source]

SHA256 of the refinement-determining config – the refinement lock’s config identity.

The complement of dataset_config_digest(): everything that function excludes from the per-dataset checkpoint’s identity under refinement (optimizer / steps / trainable / thickness_nn / split) is exactly what determines the gradient-refined result on top of already-settled Plans, so this hashes the whole refinement section. split belongs here (not in the preprocess digest) because the train/validation partition is applied when the pooled plan is handed to refinement – it never shapes a checkpointed per-dataset plan. loss_metrics is a top-level ExperimentConfig field (not under refinement, so this dump never sees it): it determines the preprocess search, so it belongs solely to dataset_config_digest().

diffBloch.config.manifest.code_version() str[source][source]

The software-version identity of the compute (checkpoint validity + run-manifest stamp).

Returns diffBloch.__version__, best-effort suffixed with the git short-SHA and a .dirty marker when running inside a checkout. Falls back to the bare version in an installed wheel (no git / no repo). This full string is the stamp recorded in the run manifest and the checkpoint lock (it says exactly which build produced an artifact). The checkpoint reuse gate, however, keys only on the release __version__ (see _release()), so a committed checkpoint stays reusable across commits within a release – the SHA/.dirty detail is recorded but does not invalidate. The trade-off is a weaker guard: a physics change without a version bump reuses; release discipline plus --refresh (regenerate) is the escape hatch.

diffBloch.config.manifest.write_preprocess_lock(path: str | Path, lock: PreprocessLock) None[source][source]

Write plan.lock in a stable, human-readable form (beside experiment.lock).

diffBloch.config.manifest.read_preprocess_lock(path: str | Path) PreprocessLock[source][source]

Read a plan.lock written by write_preprocess_lock().

diffBloch.config.manifest.write_refinement_lock(path: str | Path, lock: RefinementLock) None[source][source]

Write refinement.lock in a stable, human-readable form (beside refined_structure.cif).

diffBloch.config.manifest.read_refinement_lock(path: str | Path) RefinementLock[source][source]

Read a refinement.lock written by write_refinement_lock().

diffBloch.config.manifest.preprocess_lock_status(lock: PreprocessLock, *, structure: InputLock, experimental_data: InputLock, authoritative_cell: CellParameters, ignored_rotations: tuple[int, ...], config_digest: str, code_version: str, recipe: list[RecipeStep], plan_path: str | Path, root: str | Path) Literal['reuse', 'resume', 'stale'][source][source]

How the checkpoint lock relates to the current run’s recipe – the resume verdict.

"stale" unless the non-recipe axes all match (the structure and this dataset’s input bytes, the authoritative PETS cell, the file-local ignored rotations, the dataset config digest, and the release portion of the software version – _release(), so a differing git SHA within the same release still matches) AND the .npz verifies against the lock’s ArtifactHash (a tampered/missing checkpoint is stale). Input identity compares sha256/bytes only, never ref: renaming a dataset file without changing its bytes moves its checkpoint (new stem) but a lock whose recorded ref differs while the bytes match is still the same measurement. Given those hold:

  • "reuse" when the recipe is identical – the snapshot is exactly this run’s output.

  • "resume" when the lock’s recipe is a proper prefix of recipe – the run appends steps, so resume from the snapshot and run only the suffix (append-only / tail resume).

  • "stale" otherwise (a middle step differs, or the lock’s recipe is longer).

The caller must refuse recipes containing an opaque step before reaching here (those can never be safely reused); this function assumes a clean, comparable recipe.