Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import hashlib | |
| import math | |
| from app.schemas.visual_lesson import NucleicAcidSpec | |
| from app.services.nucleic_compiler import NucleicCompiler | |
| from app.services.nucleic_resolver import DNA_ALPHABET, RNA_ALPHABET | |
| from app.services.visual_lesson_store import VisualLessonStore | |
| class NucleicSpecValidationError(ValueError): | |
| pass | |
| class NucleicSpecValidator: | |
| def __init__(self, store: VisualLessonStore, compiler: NucleicCompiler) -> None: | |
| self.store = store | |
| self.compiler = compiler | |
| def validate(self, spec: NucleicAcidSpec) -> None: | |
| if spec.mode != "structure": | |
| if not 1 <= len(spec.sequence) <= 60: | |
| raise NucleicSpecValidationError("The displayed sequence must contain 1–60 nucleotides") | |
| alphabet = RNA_ALPHABET if spec.molecule == "rna" else DNA_ALPHABET | |
| if not set(spec.sequence) <= alphabet or ("T" in spec.sequence and "U" in spec.sequence): | |
| raise NucleicSpecValidationError("The displayed sequence is not a valid DNA or RNA IUPAC sequence") | |
| feature_ids = {feature.feature_id for feature in spec.features} | |
| claim_ids = {claim.claim_id for claim in spec.evidence_claims} | |
| if any(feature_id not in feature_ids for feature_id in spec.composition.visible_feature_ids): | |
| raise NucleicSpecValidationError("The composition references an unknown visual feature") | |
| for feature in spec.features: | |
| if not feature.claim_ids or any(claim_id not in claim_ids for claim_id in feature.claim_ids): | |
| raise NucleicSpecValidationError(f"Feature {feature.feature_id} is not linked to evidence") | |
| if spec.mode == "structure": | |
| if not spec.structure or spec.composition.primary_view != "structure": | |
| raise NucleicSpecValidationError("A molecular structure visualization requires official coordinates") | |
| path = self.store.asset_path(spec.project_id, spec.structure.coordinate_asset_id, spec.structure.coordinate_format) | |
| digest = hashlib.sha256(path.read_bytes()).hexdigest() | |
| if digest != spec.structure.coordinate_sha256 or digest != spec.structure.coordinate_asset_id: | |
| raise NucleicSpecValidationError("The molecular coordinate checksum does not match the saved asset") | |
| chain_ids = {chain.chain_id for chain in spec.structure.chains} | |
| if any(feature.chain_id and feature.chain_id not in chain_ids for feature in spec.features): | |
| raise NucleicSpecValidationError("A molecular feature references an unknown chain") | |
| elif spec.structure is not None: | |
| raise NucleicSpecValidationError("Generic nucleic geometry cannot claim an official coordinate structure") | |
| molecule = spec.nucleobase_molecule | |
| if spec.composition.primary_view == "molecule_3d" and molecule is None: | |
| raise NucleicSpecValidationError("A 3D molecule view requires verified compound coordinates") | |
| if molecule is not None: | |
| if spec.focus_base != molecule.symbol: | |
| raise NucleicSpecValidationError("The resolved nucleobase does not match the requested base") | |
| atom_ids = {atom.atom_id for atom in molecule.atoms} | |
| if not atom_ids or len(atom_ids) != len(molecule.atoms): | |
| raise NucleicSpecValidationError("The molecular atom catalogue is empty or contains duplicate IDs") | |
| if any(not all(math.isfinite(value) for value in (atom.x, atom.y, atom.z)) for atom in molecule.atoms): | |
| raise NucleicSpecValidationError("The molecular coordinates contain a non-finite value") | |
| if any( | |
| bond.atom_a not in atom_ids | |
| or bond.atom_b not in atom_ids | |
| or bond.atom_a == bond.atom_b | |
| or bond.order not in {1, 2, 3} | |
| for bond in molecule.bonds | |
| ): | |
| raise NucleicSpecValidationError("The molecular bond graph references invalid atoms") | |
| if "nucleobase-structure" not in claim_ids: | |
| raise NucleicSpecValidationError("The 3D molecule is not linked to official compound evidence") | |
| elif spec.focus_base and spec.composition.primary_view == "molecule_3d": | |
| raise NucleicSpecValidationError("The requested nucleobase has no verified 3D conformer") | |
| compiled = self.compiler.compile_spec(spec) | |
| if not compiled.assertions_passed: | |
| raise NucleicSpecValidationError("The nucleic geometry compiler assertions failed") | |