Context
Core carries a hand-ported copy of the QRF95 feature pipeline. ml/features.py,
ml/kdistance_features.py, ml/cgcnn.py, ml/cgcnn_graph.py,
ml/atom_features.py, and ml/metallicity.py are about 700 lines that exist in
stfc/goldilocks-ml as well, because that is where they were trained. They have
already drifted: the same feature contract is called
comp_struct_soap_lattice_metal.v1 in goldilocks-ml, and
qrf_comp_struct_soap_lattice_metal — no version — in Core's
model_registry.toml and in both published PSDI records.
model_registry.toml looks configuration-driven but is not. soap_r_cut,
element_property_preset, metallicity_graph_radius and the rest only mean
anything because Core hardcodes one pipeline that consumes them. A model with
different descriptors needs new Core code, and Core has to know that predicting
a k-mesh requires loading a CGCNN metallicity checkpoint — a training-time
implementation detail of one model's feature vector.
This blocks the models on the roadmap. Metallicity, magnetism, smearing, and
exchange-correlation will each need a model, and on the current shape each one
lands as new featurisation code inside Core.
Relationship to #168. #168 owns how asset files reach the disk: the asset
store, domain registries, install/verify, and container profiles. This issue
owns what happens after that — who owns featurisation and inference, and what
crosses the Core/ml boundary. Nothing here proposes a new acquisition path.
The two reinforce each other: model.json now carries the feature
configuration and the digest-pinned supporting artifacts, so the ML registry
entry #168 is designing shrinks to a task, a source, and a location.
Relationship to #134. Deleting goldilocks_core.ml.* removes most of what
that issue defers.
Problem
- Featurisation is duplicated between two repositories and has already drifted
in the contract name.
- Core must know model-internal details (a metallicity checkpoint is an input
to the k-mesh feature vector).
- Publishing a model requires a Core release, because the registry ships in
Core's wheel.
advisors/kdistance_advisor.py and advisors/kindex_advisor.py implement
the same step — a predicted quantity to a mesh — two different ways.
- Nothing checks that an artifact and the installed code agree. A k-distance
trained under one 2π convention and read under the other produces a
plausible, wrong mesh silently.
Proposed approach
One prediction type, defined by Core
Core is the consumer, so Core owns the vocabulary. goldilocks-ml satisfies it
structurally and does not import Core, so there is no dependency cycle.
@dataclass(frozen=True, slots=True)
class ModelPrediction:
parameter: str # k_points | smearing | magnetism | xc | ...
quantity: str # k_distance | k_index | degauss | functional | ...
value: float | int | bool | str | tuple[Any, ...]
target_contract: str
model_id: str
confidence: float | None = None
details: JsonDict | None = None # recorded into Provenance.details
warnings: tuple[str, ...] = () # recorded into Provenance.warnings
parameter matches the field in ParameterAdvice, so routing is a lookup, not
a branch per model. One type covers every advised parameter; a model for one
nothing covers yet adds a contract row and a resolver, not a new type.
The mesh ladder is Core's; a model picks a rung
build_kmesh_entries already enumerates every reachable mesh for a structure
and labels each with k_index, k_distance_interval, k_line_density_interval
and k_pra. That ladder is structure-determined and model-independent. One
resolver replaces both advisors:
def resolve_prediction(structure, prediction) -> KPointSelection
Two conventions must live in exactly one place there. k_distance rises as the
mesh coarsens while k_index, k_pra and k_line_density rise as it refines;
and k_distance_to_mesh uses the 2π reciprocal lattice while
mesh_to_k_line_density_interval uses the crystallographic one. Both are in
kmesh.py today and both are easy to get backwards per advisor.
The set of mesh-determining quantities is bounded by physics, unlike feature
pipelines, so Core owning it does not recreate the original problem.
A prediction is one value, never an interval
KPointSelection carries one grid, CalculationIntent has no conservatism
knob, and the study behind QRF95 reported its metrics against the median. The
interval currently survives only as prose inside provenance.reason. Choosing
which point to publish is a modelling decision; it stays with the model.
Uncertainty is demoted, not discarded. goldilocks-ml puts the interval in
details and, where a prediction is far wider than calibration led it to
expect, a sentence in warnings. Core copies both into Provenance verbatim
and branches on neither, so the judgement stays where the calibration is known.
Contracts are checked at load
load_model in goldilocks-ml already refuses an artifact whose target contract
has no defined parameter, whose trainer this build cannot serve, whose feature
schema it does not provide, whose pinned artifact digests do not match, or
whose estimator width disagrees with its own record. Each names what is
missing. An old install and a new artifact fail loudly instead of predicting
from the wrong vector.
Two upgrade paths, both leaving Core untouched
- A retrained model on the same feature contract is a data change: point the
registry at the new record.
- A new feature contract requires a
goldilocks-ml release, and the error
names it. Core still does not change.
There is no third option. Shipping code inside the artifact means unpickling
arbitrary code, and a declarative feature DSL is what model_registry.toml
already attempts.
Dependencies
Core's base install already carries dscribe, matminer, pymatgen,
scikit-learn, sklearn-quantile, torch, and torch-geometric by the
decision in #32. Depending on goldilocks-ml[qrf95] replaces those direct pins
rather than adding to them, and removes the three-way manual synchronisation of
scikit-learn==1.7.2 across Core, goldilocks-ml, and the deposit manifests.
goldilocks_ml.inference imports on a base install, so ModelPrediction and
the contract table are readable without the scientific stack.
Phases
Acceptance criteria
Upstream state
The goldilocks-ml side is implemented on feat/2-qrf95-training
(stfc/goldilocks-ml#9): goldilocks_ml.inference with ModelPrediction,
load_model, the predictor registry, digest-verified artifact resolution, and
the contract checks. It needs a release before Core can depend on it.
stfc/goldilocks-ml#10 tracks a licence conflict in the ported code that must
be settled before either repository redistributes it.
Written by an agent on behalf of Junwen Yin.
Context
Core carries a hand-ported copy of the QRF95 feature pipeline.
ml/features.py,ml/kdistance_features.py,ml/cgcnn.py,ml/cgcnn_graph.py,ml/atom_features.py, andml/metallicity.pyare about 700 lines that exist instfc/goldilocks-mlas well, because that is where they were trained. They havealready drifted: the same feature contract is called
comp_struct_soap_lattice_metal.v1in goldilocks-ml, andqrf_comp_struct_soap_lattice_metal— no version — in Core'smodel_registry.tomland in both published PSDI records.model_registry.tomllooks configuration-driven but is not.soap_r_cut,element_property_preset,metallicity_graph_radiusand the rest only meananything because Core hardcodes one pipeline that consumes them. A model with
different descriptors needs new Core code, and Core has to know that predicting
a k-mesh requires loading a CGCNN metallicity checkpoint — a training-time
implementation detail of one model's feature vector.
This blocks the models on the roadmap. Metallicity, magnetism, smearing, and
exchange-correlation will each need a model, and on the current shape each one
lands as new featurisation code inside Core.
Relationship to #168. #168 owns how asset files reach the disk: the asset
store, domain registries, install/verify, and container profiles. This issue
owns what happens after that — who owns featurisation and inference, and what
crosses the Core/ml boundary. Nothing here proposes a new acquisition path.
The two reinforce each other:
model.jsonnow carries the featureconfiguration and the digest-pinned supporting artifacts, so the ML registry
entry #168 is designing shrinks to a task, a source, and a location.
Relationship to #134. Deleting
goldilocks_core.ml.*removes most of whatthat issue defers.
Problem
in the contract name.
to the k-mesh feature vector).
Core's wheel.
advisors/kdistance_advisor.pyandadvisors/kindex_advisor.pyimplementthe same step — a predicted quantity to a mesh — two different ways.
trained under one 2π convention and read under the other produces a
plausible, wrong mesh silently.
Proposed approach
One prediction type, defined by Core
Core is the consumer, so Core owns the vocabulary. goldilocks-ml satisfies it
structurally and does not import Core, so there is no dependency cycle.
parametermatches the field inParameterAdvice, so routing is a lookup, nota branch per model. One type covers every advised parameter; a model for one
nothing covers yet adds a contract row and a resolver, not a new type.
The mesh ladder is Core's; a model picks a rung
build_kmesh_entriesalready enumerates every reachable mesh for a structureand labels each with
k_index,k_distance_interval,k_line_density_intervaland
k_pra. That ladder is structure-determined and model-independent. Oneresolver replaces both advisors:
Two conventions must live in exactly one place there.
k_distancerises as themesh coarsens while
k_index,k_praandk_line_densityrise as it refines;and
k_distance_to_meshuses the 2π reciprocal lattice whilemesh_to_k_line_density_intervaluses the crystallographic one. Both are inkmesh.pytoday and both are easy to get backwards per advisor.The set of mesh-determining quantities is bounded by physics, unlike feature
pipelines, so Core owning it does not recreate the original problem.
A prediction is one value, never an interval
KPointSelectioncarries one grid,CalculationIntenthas no conservatismknob, and the study behind QRF95 reported its metrics against the median. The
interval currently survives only as prose inside
provenance.reason. Choosingwhich point to publish is a modelling decision; it stays with the model.
Uncertainty is demoted, not discarded. goldilocks-ml puts the interval in
detailsand, where a prediction is far wider than calibration led it toexpect, a sentence in
warnings. Core copies both intoProvenanceverbatimand branches on neither, so the judgement stays where the calibration is known.
Contracts are checked at load
load_modelin goldilocks-ml already refuses an artifact whose target contracthas no defined parameter, whose trainer this build cannot serve, whose feature
schema it does not provide, whose pinned artifact digests do not match, or
whose estimator width disagrees with its own record. Each names what is
missing. An old install and a new artifact fail loudly instead of predicting
from the wrong vector.
Two upgrade paths, both leaving Core untouched
registry at the new record.
goldilocks-mlrelease, and the errornames it. Core still does not change.
There is no third option. Shipping code inside the artifact means unpickling
arbitrary code, and a declarative feature DSL is what
model_registry.tomlalready attempts.
Dependencies
Core's base install already carries
dscribe,matminer,pymatgen,scikit-learn,sklearn-quantile,torch, andtorch-geometricby thedecision in #32. Depending on
goldilocks-ml[qrf95]replaces those direct pinsrather than adding to them, and removes the three-way manual synchronisation of
scikit-learn==1.7.2across Core, goldilocks-ml, and the deposit manifests.goldilocks_ml.inferenceimports on a base install, soModelPredictionandthe contract table are readable without the scientific stack.
Phases
ModelPredictionand theStructureModelprotocol to
contracts.py, plus the parameter-to-resolver table. Nobehaviour change.
goldilocks-ml[qrf95]; add a k-meshadvisor that calls
load_modelandresolve_prediction. Keep theexisting path in place.
confirm identical meshes before anything is deleted.
kdistance_advisorandkindex_advisorwith the single ladder lookup, monotonicity handled once.
goldilocks_core.ml.*featurisation andmodel loading with their tests, and drop the now-unused direct
dependencies.
GOLDILOCKS_MODEL_REGISTRY, user config, the registry shipped withgoldilocks-ml, then Core's built-in fallback. Coordinate the file's shape
with feat(assets): unify model and pseudopotential acquisition #168.
Acceptance criteria
is missing, and a regression test covers each refusal.
Python in either repository.
import goldilocks_coreloads nogoldilocks_core.ml.*module (perf: lazy import goldilocks_core (defer ml.* / scipy) #134).the old one is removed.
uv run pytest,uv run ruff check src tests, anduv run pre-commit run --all-filesare green.Upstream state
The goldilocks-ml side is implemented on
feat/2-qrf95-training(
stfc/goldilocks-ml#9):goldilocks_ml.inferencewithModelPrediction,load_model, the predictor registry, digest-verified artifact resolution, andthe contract checks. It needs a release before Core can depend on it.
stfc/goldilocks-ml#10tracks a licence conflict in the ported code that mustbe settled before either repository redistributes it.
Written by an agent on behalf of Junwen Yin.