Source code for buildcompiler.planning.classifier

"""Design classification helpers for planning."""

from __future__ import annotations

import re
from collections import Counter

import sbol2

from buildcompiler.domain import BuildRequest, BuildStage, DesignKind
from buildcompiler.planning.models import UnsupportedPlanningRecord
from buildcompiler.planning.validation import classify_part_role, ordered_lvl1_parts

RECOMMENDED_LVL1_PARTS = ("promoter", "rbs", "cds", "terminator")


def _stable_slug(identity: str) -> str:
    return re.sub(r"[^a-z0-9]+", "-", identity.lower()).strip("-")[-48:]


[docs] def request_id_for( stage: BuildStage, source_identity: str, source_display_id: str | None, *, variant_index: int | None = None, ) -> str: base = source_display_id or _stable_slug(source_identity) rid = f"{stage.value}:{base}" if variant_index is not None: rid = f"{rid}:v{variant_index}" return rid
[docs] def classify_non_combinatorial( design: object, ) -> BuildRequest | UnsupportedPlanningRecord: if isinstance(design, sbol2.ModuleDefinition): region_identities: list[str] = [] region_part_identities: dict[str, list[str]] = {} doc = getattr(design, "doc", None) for functional_component in design.functionalComponents: region_identity = functional_component.definition if not region_identity: continue region_identities.append(region_identity) region_definition = doc.find(region_identity) if doc is not None else None if isinstance(region_definition, sbol2.ComponentDefinition): ordered_parts, _warnings = ordered_lvl1_parts(region_definition) if ordered_parts: region_part_identities[region_identity] = ordered_parts constraints = {} if region_identities: constraints["engineered_region_identities"] = region_identities if region_part_identities: constraints["lvl1_region_part_identities"] = region_part_identities return BuildRequest( request_id_for(BuildStage.ASSEMBLY_LVL2, design.identity, design.displayId), BuildStage.ASSEMBLY_LVL2, design.identity, design.displayId, DesignKind.MODULE_DEFINITION, constraints=constraints, ) if isinstance(design, sbol2.ComponentDefinition): count = len(design.components) if count > 1: observed_roles: list[str] = [] for component in design.components: target = ( component.doc.find(component.definition) if getattr(component, "doc", None) is not None else None ) if isinstance(target, sbol2.ComponentDefinition): role = classify_part_role(target) if role is not None: observed_roles.append(role) counts = Counter(observed_roles) missing = [role for role in RECOMMENDED_LVL1_PARTS if counts[role] != 1] has_role_evidence = len(observed_roles) > 0 if count != 4 or (has_role_evidence and missing): return UnsupportedPlanningRecord( design.identity, design.displayId, DesignKind.COMPONENT_DEFINITION, "Warning: Level-1 planning expects exactly four parts (promoter, RBS, CDS, terminator).", { "component_count": count, "observed_role_counts": { role: counts.get(role, 0) for role in RECOMMENDED_LVL1_PARTS }, "suggested_parts": list(RECOMMENDED_LVL1_PARTS), }, ) return BuildRequest( request_id_for( BuildStage.ASSEMBLY_LVL1, design.identity, design.displayId ), BuildStage.ASSEMBLY_LVL1, design.identity, design.displayId, DesignKind.COMPONENT_DEFINITION, ) if count <= 1 and classify_part_role(design) is not None: return BuildRequest( request_id_for( BuildStage.DOMESTICATION, design.identity, design.displayId ), BuildStage.DOMESTICATION, design.identity, design.displayId, DesignKind.COMPONENT_DEFINITION, ) return UnsupportedPlanningRecord( design.identity, design.displayId, DesignKind.COMPONENT_DEFINITION, "ComponentDefinition is single/empty with unsupported role.", {"component_count": count, "roles": list(design.roles)}, ) return UnsupportedPlanningRecord( getattr(design, "identity", str(design)), getattr(design, "displayId", None), DesignKind.UNSUPPORTED, f"Unsupported design type: {type(design).__name__}", )