File size: 2,732 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
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