Validate an experiment¶
Goal: read a ValidationReport correctly, and understand what each tier
does and does not claim.
Prerequisites: Installation.
The two entry points¶
import openauc
experiment = openauc.load("examples/data/demo_experiment")
structural = experiment.validate_structure() # ARCHIVAL + STRUCTURAL, ERROR/WARNING
everything = experiment.validate() # all four tiers, all severities
Neither raises. validate_structure() is the narrower, historical view;
validate() is the superset that also carries readiness and informational
findings.
The verdict¶
structural.is_valid # True when there are no ERROR-severity findings
structural.counts() # (errors, warnings, infos)
print(structural) # 'structural validation: OK (no issues)'
is_valid is a statement about structure only
It means the scans and observations correspond unambiguously and are internally consistent. It is never a claim that the experiment is scientifically valid, of good quality, or suitable for analysis.
The four tiers¶
| Tier | Question |
|---|---|
ARCHIVAL |
Can it be stored and returned unchanged and unambiguously? |
STRUCTURAL |
Are metadata, scans and observations internally consistent? |
SV_READINESS |
Is the metadata a future velocity workflow needs present? |
SE_READINESS |
Is the metadata a future equilibrium workflow needs present? |
A fifth question — scientific suitability — is not a tier and is never answered.
Before all of them sit construction invariants: conditions so fundamental
that an invalid object cannot be built at all. Those raise at construction and
never appear in a report — a blank scan_id, a negative elapsed time, a
non-finite radius, a mask inconsistent with its data.
Severity¶
| Severity | May block | Meaning |
|---|---|---|
ERROR |
ARCHIVAL, STRUCTURAL |
The only severity that makes is_valid false |
WARNING |
readiness tiers only | Never blocks archival or structural validity |
INFO |
nothing | Descriptive only |
Readiness findings never use ERROR. That single rule is what keeps
structural validity and analysis readiness independent.
Reading a finding¶
WARNING rotor_speed_absent: 4 scan(s) carry no rotor speed
tiers: sv_readiness, se_readiness
blocks: sv_readiness, se_readiness
component: scan.rotor_speed
observed: 4 of 4 scan(s), and no instrument nominal speed
expected: a per-scan rotor speed, or an instrument nominal speed
remediation: record rotor_speed_rpm per scan or nominal_speed_rpm
scans: scan_001, scan_002, scan_003, scan_004
tiers is what the finding speaks to; blocks is what it prevents. A
finding can pertain to a tier without blocking it — the normal case for
advisory metadata.
Worked example: structurally valid, readiness blocked¶
The commonest situation for a historical dataset. It is a success, not a failure.
from openauc.synthetic import SyntheticExperimentConfig, generate_experiment
sparse = generate_experiment(
SyntheticExperimentConfig(
scenario="sparse-metadata", n_scans=6, metadata_completeness="minimal"
)
)
assert sparse.validate_structure().is_valid # nothing wrong with it
assessment = sparse.assess_readiness()
print(assessment.sedimentation_velocity.status) # BLOCKED
for issue in assessment.sedimentation_velocity.blocking_issues:
print(" blocked by:", issue.code)
Absent metadata is reported, never required. A dataset carrying only an identifier, a radius vector and a signal vector remains archivable and structurally valid.
Worked example: archival but structurally invalid¶
broken = generate_experiment(
SyntheticExperimentConfig(scenario="invalid-structure", n_scans=4)
)
report = broken.validate_structure()
print(report.is_valid) # False
print(sorted(set(report.codes()))) # ['duplicate_scan_id', 'scan_id_mismatch']
Worked example: UNKNOWN vs OTHER experiment type¶
These are not equivalent, and the distinction drives readiness routing:
from openauc.models import ExperimentType
unknown = generate_experiment(
SyntheticExperimentConfig(experiment_type=ExperimentType.UNKNOWN, n_scans=4)
)
a = unknown.assess_readiness()
# both assessed; a non-blocking experiment_type_unknown warning is recorded
other = generate_experiment(
SyntheticExperimentConfig(experiment_type=ExperimentType.OTHER, n_scans=4)
)
b = other.assess_readiness()
print(b.sedimentation_velocity.status) # NOT_APPLICABLE
print(b.sedimentation_equilibrium.status) # NOT_APPLICABLE
OTHER is an explicit statement that the run is neither velocity nor
equilibrium. UNKNOWN is the absence of a statement, so both are assessed.
Worked example: missing provenance and optional metadata¶
from openauc.models import (
AUCExperiment, ExperimentMetadata, Observations, Quantity, ScanMetadata, Unit,
)
hand_built = AUCExperiment(
metadata=ExperimentMetadata(experiment_id="bare"),
scans=(ScanMetadata(scan_id="s1", index=0, elapsed_time=Quantity.missing()),),
observations=Observations.from_shared_axis(
radius=[6.0, 6.1], signal=[[0.1, 0.2]], scan_ids=["s1"]
),
)
report = hand_built.validate()
print(report.is_valid) # True — still perfectly valid
print("provenance_absent" in report.codes()) # True, but INFO only
provenance_absent is informational: a hand-built experiment legitimately has
none.
What is never checked¶
No convection, aggregation or sample-quality assessment. No equilibrium or meniscus detection. No molecular weight or sedimentation coefficient. No physical plausibility bounds — not even absolute zero — because that would embed scientific assumptions in a representation layer.
Next step¶
- Interpret validation findings — every code, with severity and blocked tiers
- Assess analysis readiness