File size: 4,585 Bytes
2e818da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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")