from __future__ import annotations import math from app.schemas.visual_lesson import ( CompiledNucleicForm, CompiledNucleicGeometry, NucleicAcidSpec, NucleicFormParameters, NucleicPoint, ) class NucleicCompileError(ValueError): pass class NucleicCompiler: """Pure geometry compiler for illustrative helices; never emits atomistic coordinates.""" def compile_spec(self, spec: NucleicAcidSpec) -> CompiledNucleicGeometry: if not spec.forms: return CompiledNucleicGeometry(forms=[], assertions_passed=True) compiled: list[CompiledNucleicForm] = [] for params in spec.forms: compiled.append(CompiledNucleicForm(form=params.form, points=self._compile_form(spec, params))) return CompiledNucleicGeometry(forms=compiled, assertions_passed=True) @staticmethod def _compile_form(spec: NucleicAcidSpec, params: NucleicFormParameters) -> list[NucleicPoint]: if not spec.sequence or len(spec.sequence) > 60: raise NucleicCompileError("A nucleic-acid sequence must contain between 1 and 60 symbols") if params.bases_per_turn <= 0 or params.rise_angstrom <= 0 or params.radius_angstrom <= 0: raise NucleicCompileError("Helix parameters must be positive") if not 0 < params.strand_offset_degrees < 360: raise NucleicCompileError("The strand angular offset must be between 0 and 360 degrees") direction = -1.0 if params.handedness == "left" else 1.0 points: list[NucleicPoint] = [] for index, base in enumerate(spec.sequence): display_base = "U" if params.form == "rna" and base == "T" else base angle = direction * 2.0 * math.pi * index / params.bases_per_turn z = (index - (len(spec.sequence) - 1) / 2.0) * params.rise_angstrom a = [params.radius_angstrom * math.cos(angle), params.radius_angstrom * math.sin(angle), z] opposite = angle + math.radians(params.strand_offset_degrees) b = [] if params.form == "rna" else [ params.radius_angstrom * math.cos(opposite), params.radius_angstrom * math.sin(opposite), z, ] values = [*a, *b] if any(not math.isfinite(value) for value in values): raise NucleicCompileError("Compiled helix geometry contains a non-finite coordinate") points.append(NucleicPoint( index=index, base=display_base, complement=spec.complement[index] if params.form != "rna" and index < len(spec.complement) else "", strand_a=a, strand_b=b, )) return points