Source code for buildcompiler.planning.validation

"""Deterministic validation helpers for level-1 designs."""

from __future__ import annotations

from collections import Counter

import sbol2

from buildcompiler.constants import PART_ROLES
from buildcompiler.domain import BuildStage, BuildWarning

ROLE_TO_NAME = {
    "http://identifiers.org/so/SO:0000167": "promoter",
    "http://identifiers.org/so/SO:0000139": "rbs",
    "http://identifiers.org/so/SO:0000316": "cds",
    "http://identifiers.org/so/SO:0000141": "terminator",
}
CANONICAL_ROLE_ORDER = ["promoter", "rbs", "cds", "terminator"]


[docs] def classify_part_role(component_definition: sbol2.ComponentDefinition) -> str | None: matches = sorted( ROLE_TO_NAME[role] for role in component_definition.roles if role in PART_ROLES ) if len(matches) != 1: return None return matches[0]
[docs] def validate_lvl1_cardinality( component_definition: sbol2.ComponentDefinition, ) -> tuple[bool, list[BuildWarning]]: warnings: list[BuildWarning] = [] if len(component_definition.components) != 4: warnings.append( BuildWarning( code="lvl1.variable_length", message="Level-1 v1 requires exactly four components.", stage=BuildStage.ASSEMBLY_LVL1, source_identity=component_definition.identity, ) ) return False, warnings roles = [] for comp in component_definition.components: target = ( comp.doc.find(comp.definition) if getattr(comp, "doc", None) is not None else None ) if not isinstance(target, sbol2.ComponentDefinition): continue role = classify_part_role(target) if role is not None: roles.append(role) counts = Counter(roles) ok = True for required in CANONICAL_ROLE_ORDER: if counts[required] != 1: ok = False warnings.append( BuildWarning( code="lvl1.invalid_cardinality", message=f"Expected exactly one {required}, found {counts[required]}.", stage=BuildStage.ASSEMBLY_LVL1, source_identity=component_definition.identity, metadata={"role": required, "count": counts[required]}, ) ) return ok, warnings
[docs] def ordered_lvl1_parts( component_definition: sbol2.ComponentDefinition, ) -> tuple[list[str], list[BuildWarning]]: warnings: list[BuildWarning] = [] role_to_identity: dict[str, str] = {} comp_by_identity: dict[str, sbol2.Component] = {} for comp in component_definition.components: target = ( comp.doc.find(comp.definition) if getattr(comp, "doc", None) is not None else None ) if isinstance(target, sbol2.ComponentDefinition): role = classify_part_role(target) if role: role_to_identity[role] = target.identity comp_by_identity[target.identity] = comp if len(role_to_identity) < 4: return [ role_to_identity[r] for r in CANONICAL_ROLE_ORDER if r in role_to_identity ], warnings try: ordered_components = list(component_definition.getInSequentialOrder()) except Exception: ordered_components = [] if len(ordered_components) == 4: ordered_part_ids = [] ordered_roles = [] for comp in ordered_components: target = ( comp.doc.find(comp.definition) if getattr(comp, "doc", None) is not None else None ) if isinstance(target, sbol2.ComponentDefinition): role = classify_part_role(target) if role: ordered_roles.append(role) ordered_part_ids.append(target.identity) if len(ordered_part_ids) == 4 and set(ordered_roles) == set( CANONICAL_ROLE_ORDER ): if ordered_roles != CANONICAL_ROLE_ORDER: warnings.append( BuildWarning( code="lvl1.non_canonical_order", message="SBOL order is non-canonical and will be preserved.", stage=BuildStage.ASSEMBLY_LVL1, source_identity=component_definition.identity, metadata={"ordered_roles": ordered_roles}, ) ) return ordered_part_ids, warnings warnings.append( BuildWarning( code="lvl1.ambiguous_order", message="SBOL order unavailable or ambiguous; using canonical order.", stage=BuildStage.ASSEMBLY_LVL1, source_identity=component_definition.identity, ) ) return [role_to_identity[role] for role in CANONICAL_ROLE_ORDER], warnings