HsingMD / src /casmd /engineering /models.py
zzhaobz's picture
Deploy sanitized fictional Variant A release
4c02fbe
Raw History Blame Contribute Delete
17.2 kB
"""Data contract for trajectory-guided mutation and truncation design."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Mapping
class RegionObjective(str, Enum):
WEAKEN = "weaken"
PRESERVE = "preserve"
MONITOR = "monitor"
@dataclass(frozen=True, order=True)
class ResidueKey:
segid: str
resid: int
resname: str
def __post_init__(self) -> None:
if not self.resname.strip():
raise ValueError("resname must not be empty")
@property
def token(self) -> str:
return f"{self.segid or '_'}:{self.resid}"
def to_dict(self) -> dict[str, Any]:
return {"segid": self.segid, "resid": self.resid, "resname": self.resname}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "ResidueKey":
return cls(str(value.get("segid", "")), int(value["resid"]), str(value["resname"]))
@dataclass(frozen=True)
class RegionSpec:
region_id: str
label: str
selection: str
objective: RegionObjective
cutoff_A: float = 4.0
def __post_init__(self) -> None:
if not self.region_id.strip() or not self.label.strip() or not self.selection.strip():
raise ValueError("region_id, label, and selection must not be empty")
if self.cutoff_A <= 0:
raise ValueError("cutoff_A must be positive")
def to_dict(self) -> dict[str, Any]:
return {
"region_id": self.region_id,
"label": self.label,
"selection": self.selection,
"objective": self.objective.value,
"cutoff_A": self.cutoff_A,
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "RegionSpec":
return cls(
region_id=str(value["region_id"]),
label=str(value["label"]),
selection=str(value["selection"]),
objective=RegionObjective(value["objective"]),
cutoff_A=float(value.get("cutoff_A", 4.0)),
)
@dataclass(frozen=True)
class TruncationSpec:
truncation_id: str
segid: str
start_resid: int
end_resid: int
def __post_init__(self) -> None:
if not self.truncation_id.strip():
raise ValueError("truncation_id must not be empty")
if self.start_resid > self.end_resid:
raise ValueError("start_resid must be <= end_resid")
def contains(self, residue: ResidueKey) -> bool:
segid_matches = not self.segid or self.segid == "*" or residue.segid == self.segid
return segid_matches and self.start_resid <= residue.resid <= self.end_resid
def to_dict(self) -> dict[str, Any]:
return {
"truncation_id": self.truncation_id,
"segid": self.segid,
"start_resid": self.start_resid,
"end_resid": self.end_resid,
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "TruncationSpec":
return cls(
truncation_id=str(value["truncation_id"]),
segid=str(value.get("segid", "")),
start_resid=int(value["start_resid"]),
end_resid=int(value["end_resid"]),
)
@dataclass(frozen=True)
class EngineeringConfig:
protein_selection: str
regions: tuple[RegionSpec, ...]
protected_residues: tuple[str, ...] = field(default_factory=tuple)
mutation_budget: int = 5
min_occupancy: float = 0.05
preservation_weight: float = 1.0
risk_weight: float = 1.0
o2prime_bonus: float = 0.25
max_preservation_burden: float = 0.40
max_cumulative_risk: float = 1.50
coverage_weight: float = 0.75
truncation_coverage_gate: float = 0.50
risk_by_residue: Mapping[str, float] = field(default_factory=dict)
truncations: tuple[TruncationSpec, ...] = field(default_factory=tuple)
schema_version: int = 1
def __post_init__(self) -> None:
if not self.protein_selection.strip():
raise ValueError("protein_selection must not be empty")
if not self.regions:
raise ValueError("at least one region is required")
region_ids = [item.region_id for item in self.regions]
if len(region_ids) != len(set(region_ids)):
raise ValueError("region_id values must be unique")
if not any(item.objective == RegionObjective.WEAKEN for item in self.regions):
raise ValueError("at least one weaken region is required")
if self.mutation_budget < 1:
raise ValueError("mutation_budget must be >= 1")
for label, value in (
("min_occupancy", self.min_occupancy),
("max_preservation_burden", self.max_preservation_burden),
("truncation_coverage_gate", self.truncation_coverage_gate),
):
if not 0 <= value <= 1:
raise ValueError(f"{label} must be between 0 and 1")
if any(float(value) < 0 for value in self.risk_by_residue.values()):
raise ValueError("risk values must be non-negative")
def is_protected(self, residue: ResidueKey) -> bool:
return residue.token in self.protected_residues or f"*:{residue.resid}" in self.protected_residues
def risk_for(self, residue: ResidueKey) -> float:
return float(
self.risk_by_residue.get(
residue.token, self.risk_by_residue.get(f"*:{residue.resid}", 0.0)
)
)
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"protein_selection": self.protein_selection,
"regions": [item.to_dict() for item in self.regions],
"protected_residues": list(self.protected_residues),
"mutation_budget": self.mutation_budget,
"min_occupancy": self.min_occupancy,
"preservation_weight": self.preservation_weight,
"risk_weight": self.risk_weight,
"o2prime_bonus": self.o2prime_bonus,
"max_preservation_burden": self.max_preservation_burden,
"max_cumulative_risk": self.max_cumulative_risk,
"coverage_weight": self.coverage_weight,
"truncation_coverage_gate": self.truncation_coverage_gate,
"risk_by_residue": dict(self.risk_by_residue),
"truncations": [item.to_dict() for item in self.truncations],
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "EngineeringConfig":
return cls(
protein_selection=str(value["protein_selection"]),
regions=tuple(RegionSpec.from_dict(item) for item in value["regions"]),
protected_residues=tuple(str(item) for item in value.get("protected_residues", [])),
mutation_budget=int(value.get("mutation_budget", 5)),
min_occupancy=float(value.get("min_occupancy", 0.05)),
preservation_weight=float(value.get("preservation_weight", 1.0)),
risk_weight=float(value.get("risk_weight", 1.0)),
o2prime_bonus=float(value.get("o2prime_bonus", 0.25)),
max_preservation_burden=float(value.get("max_preservation_burden", 0.40)),
max_cumulative_risk=float(value.get("max_cumulative_risk", 1.50)),
coverage_weight=float(value.get("coverage_weight", 0.75)),
truncation_coverage_gate=float(value.get("truncation_coverage_gate", 0.50)),
risk_by_residue={
str(key): float(risk) for key, risk in value.get("risk_by_residue", {}).items()
},
truncations=tuple(
TruncationSpec.from_dict(item) for item in value.get("truncations", [])
),
schema_version=int(value.get("schema_version", 1)),
)
def to_json(self) -> str:
return json.dumps(self.to_dict(), indent=2, allow_nan=False)
@dataclass(frozen=True)
class ContactObservation:
frame_index: int
protein: ResidueKey
region_id: str
nucleotide: ResidueKey
min_distance_A: float
o2prime_contact: bool = False
@dataclass(frozen=True)
class NucleotideContact:
nucleotide: ResidueKey
occupancy: float
def to_dict(self) -> dict[str, Any]:
return {"nucleotide": self.nucleotide.to_dict(), "occupancy": self.occupancy}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "NucleotideContact":
return cls(ResidueKey.from_dict(value["nucleotide"]), float(value["occupancy"]))
@dataclass(frozen=True)
class RegionFingerprint:
protein: ResidueKey
region_id: str
contact_occupancy: float
mean_contact_distance_A: float
p10_contact_distance_A: float
contact_events: int
longest_run_frames: int
o2prime_occupancy: float
nucleotide_contacts: tuple[NucleotideContact, ...] = field(default_factory=tuple)
@property
def nucleotide_coverage(self) -> int:
return len(self.nucleotide_contacts)
def to_dict(self) -> dict[str, Any]:
return {
"protein": self.protein.to_dict(),
"region_id": self.region_id,
"contact_occupancy": self.contact_occupancy,
"mean_contact_distance_A": self.mean_contact_distance_A,
"p10_contact_distance_A": self.p10_contact_distance_A,
"contact_events": self.contact_events,
"longest_run_frames": self.longest_run_frames,
"o2prime_occupancy": self.o2prime_occupancy,
"nucleotide_contacts": [item.to_dict() for item in self.nucleotide_contacts],
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "RegionFingerprint":
return cls(
protein=ResidueKey.from_dict(value["protein"]),
region_id=str(value["region_id"]),
contact_occupancy=float(value["contact_occupancy"]),
mean_contact_distance_A=float(value["mean_contact_distance_A"]),
p10_contact_distance_A=float(value["p10_contact_distance_A"]),
contact_events=int(value["contact_events"]),
longest_run_frames=int(value["longest_run_frames"]),
o2prime_occupancy=float(value.get("o2prime_occupancy", 0.0)),
nucleotide_contacts=tuple(
NucleotideContact.from_dict(item) for item in value.get("nucleotide_contacts", [])
),
)
@dataclass(frozen=True)
class MutationCandidate:
protein: ResidueKey
suggested_mutation: str
target_engagement: float
preservation_burden: float
structural_risk: float
o2prime_signal: float
design_score: float
target_pairs: tuple[str, ...]
eligible: bool
exclusion_reason: str = ""
rationale: tuple[str, ...] = field(default_factory=tuple)
def to_dict(self) -> dict[str, Any]:
return {
"protein": self.protein.to_dict(),
"suggested_mutation": self.suggested_mutation,
"target_engagement": self.target_engagement,
"preservation_burden": self.preservation_burden,
"structural_risk": self.structural_risk,
"o2prime_signal": self.o2prime_signal,
"design_score": self.design_score,
"target_pairs": list(self.target_pairs),
"eligible": self.eligible,
"exclusion_reason": self.exclusion_reason,
"rationale": list(self.rationale),
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "MutationCandidate":
return cls(
protein=ResidueKey.from_dict(value["protein"]),
suggested_mutation=str(value["suggested_mutation"]),
target_engagement=float(value["target_engagement"]),
preservation_burden=float(value["preservation_burden"]),
structural_risk=float(value["structural_risk"]),
o2prime_signal=float(value.get("o2prime_signal", 0.0)),
design_score=float(value["design_score"]),
target_pairs=tuple(str(item) for item in value.get("target_pairs", [])),
eligible=bool(value["eligible"]),
exclusion_reason=str(value.get("exclusion_reason", "")),
rationale=tuple(str(item) for item in value.get("rationale", [])),
)
@dataclass(frozen=True)
class MutationSet:
selected: tuple[MutationCandidate, ...]
target_pair_coverage: float
preservation_burden: float
cumulative_risk: float
warnings: tuple[str, ...] = field(default_factory=tuple)
def to_dict(self) -> dict[str, Any]:
return {
"selected": [item.to_dict() for item in self.selected],
"target_pair_coverage": self.target_pair_coverage,
"preservation_burden": self.preservation_burden,
"cumulative_risk": self.cumulative_risk,
"warnings": list(self.warnings),
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "MutationSet":
return cls(
selected=tuple(MutationCandidate.from_dict(item) for item in value["selected"]),
target_pair_coverage=float(value["target_pair_coverage"]),
preservation_burden=float(value["preservation_burden"]),
cumulative_risk=float(value["cumulative_risk"]),
warnings=tuple(str(item) for item in value.get("warnings", [])),
)
@dataclass(frozen=True)
class TruncationAudit:
truncation: TruncationSpec
target_coverage_removed: float
preserve_coverage_removed: float
unwanted_interaction_remaining: float
distributed_interface_warning: bool
remaining_candidate_residues: tuple[str, ...]
flexibility_ratio: float | None
interpretation: str
def to_dict(self) -> dict[str, Any]:
return {
"truncation": self.truncation.to_dict(),
"target_coverage_removed": self.target_coverage_removed,
"preserve_coverage_removed": self.preserve_coverage_removed,
"unwanted_interaction_remaining": self.unwanted_interaction_remaining,
"distributed_interface_warning": self.distributed_interface_warning,
"remaining_candidate_residues": list(self.remaining_candidate_residues),
"flexibility_ratio": self.flexibility_ratio,
"interpretation": self.interpretation,
}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "TruncationAudit":
ratio = value.get("flexibility_ratio")
return cls(
truncation=TruncationSpec.from_dict(value["truncation"]),
target_coverage_removed=float(value["target_coverage_removed"]),
preserve_coverage_removed=float(value["preserve_coverage_removed"]),
unwanted_interaction_remaining=float(value["unwanted_interaction_remaining"]),
distributed_interface_warning=bool(value["distributed_interface_warning"]),
remaining_candidate_residues=tuple(
str(item) for item in value.get("remaining_candidate_residues", [])
),
flexibility_ratio=None if ratio is None else float(ratio),
interpretation=str(value["interpretation"]),
)
@dataclass(frozen=True)
class RegionComparison:
region_id: str
objective: RegionObjective
baseline_mass: float
variant_mass: float
retention_pct: float | None
objective_change: float | None
def to_dict(self) -> dict[str, Any]:
return {
"region_id": self.region_id,
"objective": self.objective.value,
"baseline_mass": self.baseline_mass,
"variant_mass": self.variant_mass,
"retention_pct": self.retention_pct,
"objective_change": self.objective_change,
}
@dataclass(frozen=True)
class VariantComparison:
baseline_label: str
variant_label: str
regions: tuple[RegionComparison, ...]
def to_dict(self) -> dict[str, Any]:
return {
"baseline_label": self.baseline_label,
"variant_label": self.variant_label,
"regions": [item.to_dict() for item in self.regions],
}
@dataclass(frozen=True)
class DesignReport:
config: EngineeringConfig
n_frames: int
frame_interval_ps: float | None
source_label: str
fingerprints: tuple[RegionFingerprint, ...]
candidates: tuple[MutationCandidate, ...]
mutation_set: MutationSet
truncation_audits: tuple[TruncationAudit, ...]
provenance: Mapping[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": 1,
"config": self.config.to_dict(),
"n_frames": self.n_frames,
"frame_interval_ps": self.frame_interval_ps,
"source_label": self.source_label,
"fingerprints": [item.to_dict() for item in self.fingerprints],
"candidates": [item.to_dict() for item in self.candidates],
"mutation_set": self.mutation_set.to_dict(),
"truncation_audits": [item.to_dict() for item in self.truncation_audits],
"provenance": dict(self.provenance),
}
def to_json(self) -> str:
return json.dumps(self.to_dict(), indent=2, allow_nan=False)