diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56005a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - full_build + +jobs: + core: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: | + python -m pip install -U pip + python -m pip install -e ".[test]" + python -m pip install ruff + - name: Ruff check + run: ruff check tests/conftest.py tests/unit tests/stages tests/integration + - name: Core tests + run: pytest tests/unit tests/stages tests/integration diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e22f5fb..44ce5a5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,5 +37,8 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest + env: + SBH_USERNAME: ${{ secrets.SBH_USERNAME }} + SBH_PASSWORD: ${{ secrets.SBH_PASSWORD }} run: | python -m unittest discover -s tests diff --git a/ADR-001.md b/ADR-001.md new file mode 100644 index 0000000..076a6ff --- /dev/null +++ b/ADR-001.md @@ -0,0 +1,160 @@ +# ADR-001: Replace Prototype BuildCompiler Structure with Clean Architecture + +## Status + +Accepted. + +## Context + +The current BuildCompiler prototype proves important pieces of the desired workflow, especially level-1 assembly and SBOL digestion/ligation behavior. However, the current structure mixes too many responsibilities in a small number of files: + +- collection indexing +- SynBioHub pulling +- plasmid/backbone/reagent tracking +- domestication +- level-1 assembly +- placeholder level-2 assembly +- transformation +- plating +- PUDU JSON conversion +- manual protocol helpers +- Opentrons simulation helpers + +The new `full_build` vision requires a dependency-resolving compiler pipeline, partial success semantics, route optimization, structured blockers, approvals, graph/reporting, and optional protocol generation. Preserving old APIs and module boundaries would slow implementation and make Codex work harder to verify. + +## Decision + +Rewrite the internals around a clean architecture while keeping only the root package name `buildcompiler`. + +Do not preserve legacy APIs, compatibility wrappers, or old module boundaries. + +The target architecture separates: + +```text +api + public compiler object and options + +domain + stable contracts and normalized domain records + +planning + design classification, validation, combinatorial expansion + +execution + worklist and bounded full-build loop + +stages + biological build operations + +sbol + SBOL resolver and SBOL construction services + +inventory + indexed material lookup and compatibility selection + +adapters + PUDU and Opentrons handoff boundaries + +reporting + graph, summary, report, serialization +``` + +## Options considered + +### Option 1: Incrementally patch the existing `BuildCompiler` class + +Pros: + +- Smaller initial diff. +- May preserve existing notebooks temporarily. +- Easier to start quickly. + +Cons: + +- Reinforces the current god-class structure. +- Makes partial success and structured blockers awkward. +- Makes Codex tasks harder to scope. +- Encourages hidden compatibility behavior. +- Risks coupling PUDU/Opentrons side effects to compiler-only mode. + +### Option 2: Preserve public wrappers but move internals + +Pros: + +- Gives users a transition path. +- Reduces immediate breakage. + +Cons: + +- Adds compatibility traces the project does not need. +- Doubles API surface during a major refactor. +- Forces new contracts to fit old return shapes. +- Makes partial success harder to expose cleanly. + +### Option 3: Clean architecture rewrite with no compatibility traces + +Pros: + +- Best long-term module boundaries. +- Makes Codex tasks smaller and safer. +- Enables testable contracts before complex stages. +- Separates compiler-only mode from automation side effects. +- Treats expected build blockers as structured data. +- Supports route optimization, reporting, and approvals naturally. + +Cons: + +- Larger initial migration. +- Existing notebooks/imports will break. +- Requires disciplined milestone sequencing. +- Requires new documentation and tests early. + +## Rationale + +Option 3 is best now because the user explicitly prefers clean architecture over compatibility and wants the current repository treated as inspiration, not an API to preserve. + +The new `full_build` behavior is not a small extension. It is a compiler pipeline with: + +- planning +- inventory lookup +- route optimization +- staged execution +- retry loops +- structured missing inputs +- approval gates +- SBOL artifacts +- PUDU JSON adapters +- reporting + +Those concerns need explicit boundaries. + +## Consequences + +- The root package remains `buildcompiler`. +- Existing method signatures such as `compiler.assembly_lvl1(raw_component_definition)` do not need to survive. +- `BuildCompiler.__init__` becomes dependency-injected and lightweight. +- `BuildCompiler.from_synbiohub(...)` owns convenience loading/indexing. +- Stages use a uniform `run(request, context) -> StageResult` contract. +- Inventory records replace the old `Plasmid` class. +- PUDU/Opentrons are optional adapters, not core dependencies. +- Codex should implement in milestone order and keep tests passing at each step. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Rewrite grows too large | Implement milestone-by-milestone with tests. | +| Working level-1 assembly regresses | Port current `Assembly` behavior mostly intact before deeper cleanup. | +| Users lose old APIs | Accepted tradeoff; update docs and examples for new API. | +| SBOL identity bugs | Use identity-based domain contracts and central `SbolResolver`. | +| Route optimizer is hard to debug | Use explicit score dataclasses and detailed report alternatives. | +| Automation dependencies destabilize core CI | Keep PUDU/Opentrons optional and out of default CI. | + +## Revisit triggers + +Revisit this decision if: + +- A downstream user requires stable old APIs for publication or production workflows. +- SynBioSuite integration depends on old imports. +- The clean rewrite cannot pass level-1 fixture tests after the assembly service port. +- A future release needs a compatibility bridge for migration. diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..bda13fb --- /dev/null +++ b/AGENT.md @@ -0,0 +1,402 @@ +# Agent Guide for BuildCompiler + +## Mission + +Codex should implement the BuildCompiler clean-architecture refactor in small, verifiable increments. The goal is not to preserve legacy APIs. The current repository is inspiration and evidence, especially for level-1 assembly and SBOL digestion/ligation behavior. + +## Role split + +### ChatGPT owns + +- Reducing ambiguity. +- Revising architecture and product decisions. +- Breaking work into milestones and tasks. +- Deciding between alternatives when requirements conflict. +- Reviewing whether Codex output matches the intended plan. +- Updating this guidance when product or architecture decisions change. + +### Codex owns + +- Creating and editing repository files. +- Implementing scoped tasks. +- Refactoring within approved boundaries. +- Adding tests. +- Running checks. +- Reporting assumptions, blockers, and deviations. + +When a decision changes architecture, product scope, biological behavior, or public API semantics, Codex should stop and ask ChatGPT/user for a decision before continuing. + +## Hard rules + +1. Prefer small, incremental changes. +2. Keep the project runnable after every task. +3. Write tests for non-trivial logic. +4. Update docs when architecture, behavior, or commands change. +5. Do not preserve legacy APIs unless explicitly requested. +6. Do not add compatibility wrappers. +7. Do not silently expand scope. +8. Do not hide assumptions. +9. Do not make PUDU or Opentrons required for core tests. +10. Do not run Opentrons simulation by default. +11. Do not silently mutate biological sequences. +12. Do not silently assume reagent purchase. +13. Keep domain contracts identity-based where possible. +14. Keep raw `sbol2` objects at inventory/SBOL/stage boundaries. +15. Use `SbolResolver` instead of scattered direct SBOL document lookup. +16. Use structured blockers, warnings, and approvals instead of raw exceptions for expected build issues. +17. Raise unexpected implementation errors by default. + +## Definition of a safe Codex task + +A task is safe for Codex when it has: + +- A clear target file or module. +- A clear expected behavior. +- A test strategy. +- No unresolved product or architecture tradeoff. +- No need to choose between incompatible biological semantics. + +Examples: + +- Add `StageStatus`, `BuildStatus`, and tests. +- Implement `BuildOptions` dataclasses. +- Create `SbolResolver` with `PullPolicy.NEVER` unit tests. +- Add `Inventory.add_generated_product()` and indexing tests. +- Port `Assembly` behind `AssemblyService` without changing internals. + +## Definition of a risky task + +A task is risky when it: + +- Changes public API semantics. +- Changes build-stage behavior. +- Changes approval or sequence-edit policy. +- Changes route-selection ranking. +- Changes material-state semantics. +- Touches PUDU/Opentrons execution side effects. +- Requires choosing how SBOL structures should be interpreted. +- Would take more than one focused PR. + +Risky tasks should be decomposed or escalated to ChatGPT/user. + +## Handoff protocol + +Every Codex task should begin with: + +```text +Task goal: +Files expected to change: +Behavior expected: +Tests expected: +Known constraints: +``` + +Every Codex completion should report: + +```text +Implemented: +Tests added/updated: +Commands run: +Assumptions made: +Blockers or follow-up tasks: +``` + +If tests cannot be run, Codex must say why and identify the smallest command that should be run later. + +## Implementation sequence + +Implement in milestone order. Do not jump directly to the full-build loop before contracts and tests exist. + +### Milestone 1: Domain contracts and options + +Create: + +```text +src/buildcompiler/domain/ +src/buildcompiler/api/options.py +``` + +Add contracts for: + +- `BuildStage` +- `StageStatus` +- `BuildStatus` +- `MaterialState` +- `BuildRequest` +- `StageResult` +- `FullBuildResult` +- `MissingBuildInput` +- `RequiredApproval` +- `BuildWarning` +- `IndexedPlasmid` +- `IndexedBackbone` +- `IndexedReagent` +- `BuildOptions` and focused option groups + +Tests: + +```text +tests/unit/domain/ +tests/unit/api/test_options.py +``` + +### Milestone 2: SBOL resolver and inventory + +Create: + +```text +src/buildcompiler/sbol/resolver.py +src/buildcompiler/inventory/ +``` + +Implement: + +- `PullPolicy` +- `SbolResolver` +- normalized inventory records +- eager indexes +- generated-product indexing +- reagent lookup +- backbone lookup + +Tests should use offline SBOL fixtures and `PullPolicy.NEVER`. + +### Milestone 3: Planner and classification + +Create: + +```text +src/buildcompiler/planning/classifier.py +src/buildcompiler/planning/combinatorial.py +src/buildcompiler/planning/full_build_planner.py +src/buildcompiler/planning/validation.py +``` + +Implement classification rules: + +```text +ModuleDefinition -> assembly_lvl2 +CombinatorialDerivation -> expanded assembly_lvl1 requests +ComponentDefinition >1 component -> assembly_lvl1 +ComponentDefinition <=1 component with supported role -> domestication +ComponentDefinition <=1 component unsupported -> unsupported +``` + +Implement combinatorial cap and invalid variant warnings. + +### Milestone 4: Compatibility selector and route models + +Create: + +```text +src/buildcompiler/inventory/selector.py +src/buildcompiler/inventory/compatibility.py +``` + +Implement route scoring dataclasses for level-1 and level-2. Keep scores explicit, not opaque numbers. + +### Milestone 5: SBOL assembly service and level-1 stage + +Port current working `Assembly` behavior into: + +```text +src/buildcompiler/sbol/assembly.py +``` + +Wrap it with: + +```text +src/buildcompiler/stages/assembly_lvl1.py +``` + +Do not deeply refactor SBOL internals until tests protect the new service interface. + +### Milestone 6: Domestication stage + +Create: + +```text +src/buildcompiler/sbol/domestication.py +src/buildcompiler/stages/domestication.py +``` + +Implement: + +- supported role validation +- missing backbone handling +- missing reagent handling +- sequence edit proposals +- approval-gated execution behavior +- domesticated plasmid output + +### Milestone 7: Level-2 stage + +Create: + +```text +src/buildcompiler/stages/assembly_lvl2.py +``` + +Implement: + +- `ModuleDefinition` engineered-region extraction +- optional `region_order` constraint +- exhaustive order search up to 4 regions +- missing level-1 promotion +- selected route and top rejected alternatives + +### Milestone 8: Full-build executor + +Create: + +```text +src/buildcompiler/execution/full_build_executor.py +src/buildcompiler/execution/worklist.py +src/buildcompiler/execution/stage_runner.py +src/buildcompiler/execution/context.py +``` + +Implement bounded retry loop with mocked/stubbed stages first, then wire real stages. + +### Milestone 9: Transformation and plating + +Create: + +```text +src/buildcompiler/stages/transformation.py +src/buildcompiler/stages/plating.py +src/buildcompiler/sbol/transformation.py +``` + +Implement SBOL transformation records, PUDU JSON intermediates, plate mapping, and deduplication. + +### Milestone 10: Adapters, reporting, and integration tests + +Create: + +```text +src/buildcompiler/adapters/pudu/ +src/buildcompiler/adapters/opentrons/ +src/buildcompiler/reporting/ +``` + +Implement: + +- assembly/transformation/plating JSON adapters +- optional manual/automated protocol file generation +- optional Opentrons simulation wrapper +- `BuildSummary` +- opt-in `BuildReport` +- reporting-only `BuildGraph` +- end-to-end happy-path fixture test + +## Testing and verification expectations + +Use pytest and Ruff for default CI. + +Core commands: + +```bash +uv run ruff check . +uv run ruff format --check . +uv run pytest +``` + +Fallback without `uv`: + +```bash +ruff check . +ruff format --check . +pytest +``` + +Automation-specific tests should be marked and optional: + +```bash +pytest -m automation +``` + +Suggested test layout: + +```text +tests/ + unit/ + domain/ + planning/ + inventory/ + execution/ + reporting/ + stages/ + test_assembly_lvl1.py + test_domestication.py + test_assembly_lvl2.py + test_transformation.py + test_plating.py + integration/ + test_full_build_happy_path.py + test_full_build_missing_lvl1_then_domestication.py + automation/ + test_pudu_json_adapters.py + test_opentrons_simulation_smoke.py + fixtures/ + sbol/ + expected_json/ +``` + +## Documentation update rules + +Update docs in the same PR when: + +- public API changes +- package layout changes +- option defaults change +- stage behavior changes +- status semantics change +- approval behavior changes +- route scoring changes +- test commands or CI commands change + +## ADR trigger conditions + +Create or update an ADR when: + +- There are multiple reasonable architecture options. +- A decision changes module boundaries. +- A decision changes biological interpretation. +- A decision changes public API. +- A decision adds or removes a stage. +- A decision changes default safety behavior. +- A decision adds persistent side effects. + +## Progress reporting style + +Codex should keep progress compact and evidence-based: + +```text +Done: +- Added domain status enums and tests. +- Added BuildOptions groups with defaults. + +Verified: +- uv run pytest tests/unit/domain tests/unit/api +- uv run ruff check src tests + +Assumptions: +- MaterialState.PLANNED ranks below ASSEMBLED and above missing material. + +Next: +- Implement SbolResolver with PullPolicy.NEVER tests. +``` + +## When to stop and ask + +Stop and ask ChatGPT/user before: + +- Changing approval semantics. +- Changing level-1 cardinality. +- Supporting new part roles. +- Changing level-2 route scoring. +- Making graph scheduling drive execution. +- Making PUDU/Opentrons required. +- Adding persistent approvals. +- Adding DNA extraction as a real stage. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d0f1a31 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# Developer Guide for AI Agents + +## ๐Ÿง  Mental Model + +BuildCompiler is a **workflow orchestrator**. + +Each function: +- Receives structured input +- Produces structured output +- Does not rely on hidden state + +--- + +## ๐Ÿ”‘ Core Entry Points + +- `index_collections` +- `domestication` +- `assembly_lvl1` +- `assembly_lvl2` +- `transformation` +- `plating` +- `full_build` + +--- + +## ๐Ÿ“ฆ Contracts + +Each function should: + +INPUT: +- SBOL or JSON + +OUTPUT: +- SBOL +- JSON +- Protocol files + +--- + +## โš ๏ธ Constraints + +- No hidden side effects +- Deterministic outputs +- Explicit file outputs + +--- + +## ๐Ÿงช Testing Expectations + +Each module must: +- Work independently +- Be testable with mock SBOL +- Validate outputs + +--- + +## ๐ŸŽฏ Goals for Contributions + +- Improve modularity +- Maintain pipeline clarity +- Avoid coupling biology logic with execution logic diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..801e6ca --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,599 @@ +# BuildCompiler Architecture + +## System overview + +BuildCompiler is a Python compiler pipeline for synthetic biology build planning. It takes SBOL designs and biological inventory, then produces structured build plans, SBOL build artifacts, PUDU-compatible JSON intermediates, and optional protocol-generation artifacts. + +The architecture separates: + +```text +planning: classify and expand what should be built +execution: decide what to run and retry +stages: perform biological build operations +inventory: answer what material exists +sbol: construct and resolve SBOL objects +adapters: translate BuildCompiler outputs to external formats +reporting: explain what happened and what to do next +``` + +## Design principles + +1. **Compiler first, automation second.** Core planning and SBOL/JSON generation should run without PUDU or Opentrons. +2. **Contracts before implementation.** Domain dataclasses define stable boundaries between planner, executor, stages, inventory, and reporting. +3. **Partial success is normal.** Stages may produce some products while reporting missing inputs or approvals. +4. **Expected blockers are data.** Missing plasmids, backbones, reagents, approvals, and unsupported designs should be structured records. +5. **Unexpected bugs are errors.** Implementation errors should raise by default unless `continue_on_error=True`. +6. **Inventory and selection are separate.** Inventory answers what exists; selectors decide which valid route to use. +7. **SBOL identity is canonical.** Domain models should use SBOL identities as stable keys. Raw `sbol2` objects stay at inventory/SBOL/stage boundaries. +8. **The graph explains; it does not schedule.** The v1 build graph is reporting-only. + +## Package structure + +```text +src/buildcompiler/ + api/ Public user API and options + domain/ Pure contracts and normalized domain models + planning/ Classification, validation, combinatorial expansion + execution/ Worklist, context, bounded full-build loop + stages/ Domestication, lvl1, lvl2, transformation, plating + sbol/ SBOL construction, resolver, document helpers + inventory/ Indexes, SynBioHub loading, compatibility selection + adapters/ PUDU JSON/protocol and Opentrons simulation adapters + reporting/ Build graph, summary, detailed report, serialization + errors.py + logging.py +``` + +## Public API + +`BuildCompiler.__init__` is dependency-injected and lightweight: + +```python +compiler = BuildCompiler( + inventory=inventory, + sbol_document=doc, + planner=FullBuildPlanner(), + executor=FullBuildExecutor(), + adapters=AdapterRegistry(...), +) +``` + +Convenience construction handles SynBioHub loading and indexing: + +```python +compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) +``` + +Primary workflow: + +```python +plan = compiler.plan(abstract_designs) +result = compiler.execute(plan) +``` + +Convenience wrapper: + +```python +result = compiler.full_build(abstract_designs, options=options) +``` + +## Core domain model + +### Status enums + +```python +class StageStatus(Enum): + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + BLOCKED = "blocked" + FAILED = "failed" + +class BuildStatus(Enum): + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + FAILED = "failed" +``` + +`BLOCKED` means expected inputs or approvals can unblock the stage later. `FAILED` means the request cannot proceed without changing the design, options, collections, or approval state. + +### BuildRequest + +```python +@dataclass +class BuildRequest: + id: str + stage: BuildStage + source_identity: str + source_display_id: str | None + source_kind: DesignKind + parent_group: str | None = None + variant_index: int | None = None + constraints: dict = field(default_factory=dict) +``` + +Core contracts should use SBOL identities and normalized metadata. Stages resolve identities to raw SBOL objects through `SbolResolver`. + +### StageResult + +```python +@dataclass +class StageResult: + id: str + stage: BuildStage + status: StageStatus + request_ids: list[str] + products: list[IndexedPlasmid] + missing_inputs: list[MissingBuildInput] + required_approvals: list[RequiredApproval] + warnings: list[BuildWarning] + sbol_document: sbol2.Document | None + json_intermediate: dict | list | None + protocol_artifacts: dict + logs: list[str] +``` + +### FullBuildResult + +```python +@dataclass +class FullBuildResult: + status: BuildStatus + plan: BuildPlan + build_document: sbol2.Document + stage_results: list[StageResult] + graph: BuildGraph + final_products: list[IndexedPlasmid] + missing_inputs: list[MissingBuildInput] + required_approvals: list[RequiredApproval] + warnings: list[BuildWarning] + summary: BuildSummary + report: BuildReport | None = None +``` + +### MissingBuildInput + +```python +@dataclass +class MissingBuildInput: + source_stage: BuildStage + source_design_identity: str + missing_identity: str + missing_display_id: str | None + missing_kind: Literal[ + "engineered_region", + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + ] + required_stage: BuildStage | Literal["fatal"] + reason: str + candidates_tried: list[str] = field(default_factory=list) +``` + +### RequiredApproval + +Approvals should be easy to grant by process name. + +```python +@dataclass +class RequiredApproval: + status: ApprovalStatus + process: str + reason: str + metadata: dict = field(default_factory=dict) + +@dataclass +class ApprovalOptions: + approved_processes: set[str] = field(default_factory=set) + approved_approval_ids: set[str] = field(default_factory=set) + scope: Literal["run", "persistent"] = "run" +``` + +Default approval scope is run-scoped. + +## Options model + +Use composed option groups: + +```python +@dataclass +class BuildOptions: + planning: PlanningOptions = field(default_factory=PlanningOptions) + execution: ExecutionOptions = field(default_factory=ExecutionOptions) + selection: SelectionOptions = field(default_factory=SelectionOptions) + protocol: ProtocolOptions = field(default_factory=ProtocolOptions) + reporting: ReportingOptions = field(default_factory=ReportingOptions) + approvals: ApprovalOptions = field(default_factory=ApprovalOptions) + reagents: ReagentOptions = field(default_factory=ReagentOptions) + domestication: DomesticationOptions = field(default_factory=DomesticationOptions) +``` + +Important defaults: + +```python +ExecutionOptions(max_iterations=5, continue_on_error=False) +ProtocolOptions(mode=ProtocolMode.NONE, simulate=False, results_dir=None) +ReportingOptions(include_detailed_report=False, include_rejected_routes=True, max_rejected_routes=3) +CombinatorialOptions(max_variants=256, allow_large_expansion=False) +Lvl2SearchOptions(max_exhaustive_region_count=4, allow_large_order_search=False) +ReagentOptions(allow_reagent_purchase=False, default_restriction_enzyme="BsaI", default_ligase="T4_DNA_ligase") +``` + +## BuildContext + +Every stage receives one explicit typed context: + +```python +@dataclass +class BuildContext: + sbol: SbolResolver + inventory: Inventory + build_document: sbol2.Document + options: BuildOptions + adapters: AdapterRegistry + graph: BuildGraph + logger: BuildLogger +``` + +Stage signature: + +```python +def run(request: BuildRequest, context: BuildContext) -> StageResult: + ... +``` + +## SBOL resolver + +Stages should not call `doc.get(...)`, `doc.find(...)`, or SynBioHub pull logic directly. Use `SbolResolver`. + +```python +class SbolResolver: + def get_component(self, identity: str) -> sbol2.ComponentDefinition: ... + def get_module(self, identity: str) -> sbol2.ModuleDefinition: ... + def get_combinatorial_derivation(self, identity: str) -> sbol2.CombinatorialDerivation: ... + def get_implementation(self, identity: str) -> sbol2.Implementation: ... + def maybe_pull(self, identity: str) -> object: ... +``` + +Pull policy: + +```python +class PullPolicy(Enum): + NEVER = "never" + MISSING_ONLY = "missing_only" + ALWAYS_REFRESH = "always_refresh" +``` + +Default to `MISSING_ONLY`; use `NEVER` for deterministic offline tests. + +## Inventory architecture + +Inventory owns normalized records and eager indexes. + +### IndexedPlasmid + +```python +@dataclass +class IndexedPlasmid: + identity: str + display_id: str + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + strain_definitions: list[sbol2.ModuleDefinition] + strain_implementations: list[sbol2.Implementation] + insert_identities: set[str] + fusion_sites: tuple[str, ...] + antibiotic_resistance: str | None + source: Literal["collection", "generated"] + generating_stage: BuildStage | None + material_state: MaterialState + provenance: dict +``` + +### IndexedBackbone and IndexedReagent + +```python +@dataclass +class IndexedBackbone: + identity: str + display_id: str + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + fusion_sites: tuple[str, ...] + antibiotic_resistance: str | None + material_state: MaterialState + source: Literal["collection", "generated"] + provenance: dict + +@dataclass +class IndexedReagent: + identity: str + display_id: str + name: str + kind: Literal["restriction_enzyme", "ligase", "other"] + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + source: Literal["collection", "generated", "assumed_purchase"] + provenance: dict +``` + +### Inventory facade + +```python +class Inventory: + plasmids_by_identity: dict[str, IndexedPlasmid] + plasmids_by_insert_identity: dict[str, list[IndexedPlasmid]] + plasmids_by_fusion_sites: dict[tuple[str, ...], list[IndexedPlasmid]] + plasmids_by_antibiotic: dict[str, list[IndexedPlasmid]] + backbones_by_fusion_sites_and_antibiotic: dict[tuple[tuple[str, ...], str], list[IndexedBackbone]] + + def find_single_part_plasmids(self, part_identity: str, *, antibiotic: str | None = None) -> list[IndexedPlasmid]: ... + def find_lvl1_region_plasmids(self, region_identity: str, *, min_material_state: MaterialState = MaterialState.PLANNED) -> list[IndexedPlasmid]: ... + def find_backbone(self, *, fusion_sites: tuple[str, ...] | None = None, antibiotic: str | None = None, stage: BuildStage | None = None) -> IndexedBackbone | None: ... + def find_restriction_enzyme(self, name: str) -> IndexedReagent | None: ... + def find_ligase(self, preferred: str | None = None) -> IndexedReagent | None: ... + def add_generated_product(self, product: IndexedPlasmid) -> None: ... +``` + +Inventory builds indexes eagerly and updates incrementally when generated products are added. + +## Compatibility selection + +`CompatibilitySelector` answers which valid route to use. Inventory should not own route-ranking policy. + +Default ranking: + +1. Respect request constraints. +2. Respect global selection options. +3. Prefer fewer new build products required. +4. Prefer existing collection material over generated/planned material. +5. Prefer higher material state. +6. Prefer exact antibiotic/fusion-site match. +7. Prefer simpler material routes. +8. Tie-break by stable SBOL identity. + +Selection overrides live in both `BuildOptions.selection` and `BuildRequest.constraints`. Request constraints win over global options. + +## Planning flows + +### Design classification + +`FullBuildPlanner` should classify designs into initial queues: + +```text +ModuleDefinition -> assembly_lvl2 +CombinatorialDerivation -> expand into assembly_lvl1 variant requests +ComponentDefinition with >1 component -> assembly_lvl1 +ComponentDefinition with <=1 component and supported role -> domestication +ComponentDefinition with <=1 component and unsupported role -> unsupported +``` + +### Combinatorial expansion + +- Expand valid concrete variants into level-1 requests. +- Validate each variant against level-1 cardinality. +- Skip invalid variants with structured warnings. +- Fail only if no valid variants remain. +- Default cap: `max_variants=256` unless `allow_large_expansion=True`. + +## Execution flow + +Use a bounded worklist/fixed-point loop: + +```python +for iteration in range(options.execution.max_iterations): + progress = False + + lvl2_results = run_buildable_lvl2_requests() + progress |= index_products(lvl2_results) + promote_missing_engineered_regions_to_lvl1(lvl2_results) + + lvl1_results = run_buildable_lvl1_requests() + progress |= index_products(lvl1_results) + promote_missing_parts_to_domestication(lvl1_results) + + domestication_results = run_buildable_domestication_requests() + progress |= index_products(domestication_results) + + chain_transformation_and_plating_for_new_products() + + if all_requests_resolved(): + break + if not progress: + break +``` + +Safeguards: + +- `max_iterations = 5` by default. +- Track `seen_products`, `seen_missing_inputs`, and `seen_requests`. +- Transform/plate each unique product only once. +- Raise unexpected implementation errors by default unless `continue_on_error=True`. + +## Stage responsibilities + +### Domestication stage + +Responsibilities: + +- Validate supported part role. +- Find required domestication backbone. +- Find BsaI and ligase reagents or return missing inputs. +- Propose internal BsaI sequence edits. +- Require approval in protocol/execution mode before using edited sequence. +- Create domesticated plasmid as primary product. +- Record insert and sequence-edit provenance as artifacts. + +Primary product: domesticated plasmid. + +### Assembly level 1 stage + +Responsibilities: + +- Resolve concrete level-1 design request. +- Extract exactly one promoter, one RBS, one CDS, one terminator. +- Use SBOL order if unambiguous. +- Warn if SBOL order differs from promoter/RBS/CDS/terminator. +- Use canonical order if SBOL order is ambiguous. +- Search candidate part plasmids and backbones. +- Select route minimizing new domestications. +- Return products or missing parts. +- Index products by plasmid identity and source engineered-region/design identity. + +### Assembly level 2 stage + +Responsibilities: + +- Resolve `ModuleDefinition` with engineered-region components. +- Use `region_order` constraint if provided. +- If order absent and region count <= 4, enumerate candidate orders. +- If no full route exists, choose route requiring fewest new level-1 plasmids. +- Return selected route plus top 3 rejected alternatives when detailed reporting is enabled. +- Return missing engineered regions as level-1 requests. + +### Transformation stage + +Responsibilities: + +- Consume generated plasmids. +- Create SBOL transformation records. +- Produce PUDU-compatible transformation JSON. +- Produce transformed strain records/products for plating. + +### Plating stage + +Responsibilities: + +- Consume transformed strain outputs. +- Produce deterministic well mapping. +- Return plate map dictionary as main result. +- Optionally write CSV mapping with strain implementation URIs and dilution metadata. +- Produce PUDU-compatible plating JSON. +- No SBOL output is required for plating v1. + +## SBOL assembly service + +Port the current working `Assembly` behavior mostly intact into `sbol/assembly.py`, behind a cleaner service interface. Refactor internals only after tests protect the new contract. + +```python +@dataclass +class AssemblyJob: + stage: BuildStage + product_identity: str + product_display_id: str + part_plasmids: list[IndexedPlasmid] + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: sbol2.Document + target_document: sbol2.Document + +@dataclass +class AssemblySbolResult: + products: list[IndexedPlasmid] + stage_document: sbol2.Document + activity_identity: str + logs: list[str] +``` + +## Adapter boundaries + +Adapters translate BuildCompiler outputs into external inputs. + +- Assembly PUDU JSON uses `Product`, `Backbone`, `PartsList`, and `Restriction Enzyme`. +- Transformation PUDU JSON uses `Strain`, `Chassis`, and `Plasmids`. +- Plating PUDU JSON uses `bacterium_locations` plus advanced parameters. + +Compiler-only mode always creates in-memory JSON intermediates. File writing, manual Markdown, OT-2 scripts, and simulation are optional. + +`simulate=True` is required for Opentrons simulation. Simulation is never default. + +## Reporting + +### BuildSummary + +Always generated. + +```python +@dataclass +class BuildSummary: + status: BuildStatus + final_product_count: int + missing_input_count: int + required_approval_count: int + warning_count: int + + def to_dict(self) -> dict: ... + def to_json(self) -> str: ... + def to_markdown(self) -> str: ... +``` + +### BuildReport + +Generated only when `include_detailed_report=True`. + +```python +@dataclass +class BuildReport: + status: BuildStatus + executive_summary: str + stage_sections: list[StageReportSection] + selected_routes: list[RouteReport] + rejected_alternatives: list[RouteReport] + missing_inputs: list[MissingInputReport] + required_approvals: list[ApprovalReport] + warnings: list[WarningReport] + next_actions: list[RecommendedAction] + dependency_chain: list[DependencyChainStep] + graph_summary: dict +``` + +`next_actions` tells the user what to do now. `dependency_chain` explains the path to final success. + +## Operational concerns + +### Configuration + +- Keep defaults safe and compiler-only. +- Put broad options in focused dataclasses. +- Make network pull behavior explicit through `PullPolicy`. +- Make approvals run-scoped by default. + +### Observability + +- Replace `print` calls with `BuildLogger`. +- Include per-stage logs in `StageResult`. +- Include candidate route scoring in detailed reports. + +### Security and safety + +- Do not silently mutate sequences. +- Do not silently assume reagent purchases. +- Do not silently expand very large combinatorial designs. +- Do not silently perform large level-2 order searches. +- Do not run robot simulations as side effects. + +### Testing + +Core tests should run offline where possible. SynBioHub network access should be isolated behind integration tests or explicitly marked tests. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Clean rewrite becomes too large | Use milestone sequence and require passing tests at each layer. | +| SBOL object identity bugs | Keep canonical identity-based domain contracts and centralize lookup in `SbolResolver`. | +| Route optimizer complexity grows | Start with explicit scoring dataclasses and bounded search limits. | +| Current working level-1 assembly regresses | Port mostly intact first and protect with SBOL fixture tests before internal cleanup. | +| Optional PUDU/Opentrons dependencies destabilize CI | Keep automation tests optional/manual. | +| Sequence edits are unsafe | Require approval in execution/protocol mode and record provenance. | +| Reports become too verbose | Always generate lightweight summary; detailed report is opt-in. | diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..ffed963 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,169 @@ +# Product Brief: BuildCompiler Full-Build Refactor + +## Problem + +Synthetic biology users can design constructs in SBOL, but turning those designs into executable build workflows still requires manual reasoning about available inventory, missing parts, assembly levels, transformation, plating, and automation handoff formats. + +BuildCompiler should act like a compiler: inspect abstract designs, resolve dependencies against inventory, choose an implementable route, generate missing intermediates when possible, and report actionable blockers when not possible. + +## Target users + +- Synthetic biology researchers designing constructs in SBOL/SBOLCanvas/SynBioSuite. +- Lab automation users generating manual or Opentrons-ready protocols through PUDU. +- Developers extending BuildCompiler's planning, inventory, SBOL, or protocol-generation behavior. +- AI coding agents such as Codex implementing scoped refactor tasks from an approved plan. + +## Jobs to be done + +1. As a researcher, I want to submit abstract SBOL designs and learn whether they can be built from available inventory. +2. As a build planner, I want missing level-1 engineered regions promoted into level-1 assembly requests. +3. As a build planner, I want missing promoter/RBS/CDS/terminator parts promoted into domestication requests. +4. As an automation user, I want SBOL and PUDU-compatible JSON outputs for assembly, transformation, and plating. +5. As a lab user, I want clear actions when a build is blocked by missing inventory, missing approvals, or unsupported designs. +6. As a developer, I want clear module boundaries so I can improve one stage without breaking the whole compiler. + +## Product goals + +- Make `full_build` a deterministic, inspectable compiler pipeline. +- Support partial success and structured blockers. +- Minimize new build work by searching inventory before generating new requests. +- Keep compiler-only mode lightweight and testable. +- Make optional automation outputs explicit, not default side effects. +- Provide a clean architecture that Codex can implement in small, safe increments. + +## Non-goals + +- Preserve old method signatures or module boundaries. +- Build a generic workflow engine. +- Make the reporting graph drive scheduling in v1. +- Implement DNA extraction as a real v1 stage. +- Automatically run Opentrons simulation. +- Silently mutate biological sequences without approval. +- Support arbitrary variable-length level-1 designs in v1. + +## V1 scope + +### Design classification + +Initial abstract designs are classified as: + +```text +ModuleDefinition + -> assembly_lvl2 + +CombinatorialDerivation + -> expand to concrete assembly_lvl1 variants + +ComponentDefinition with >1 component + -> assembly_lvl1 + +ComponentDefinition with <=1 component and supported role + -> domestication + +ComponentDefinition with <=1 component and unsupported role + -> unsupported planning record +``` + +Supported level-1 roles for v1 are exactly: + +```text +promoter, RBS, CDS, terminator +``` + +A valid level-1 concrete request contains exactly one promoter, one RBS, one CDS, and one terminator. + +### Full-build behavior + +`full_build` should use a bounded dependency-resolution loop: + +```text +1. Try buildable level-2 requests. +2. Promote missing engineered regions to level-1 requests. +3. Try buildable level-1 requests. +4. Promote missing promoter/RBS/CDS/terminator parts to domestication requests. +5. Try domestication requests. +6. Index generated products. +7. Retry upward until success, no progress, failure, or max_iterations. +``` + +Successful assembly/domestication products are immediately chained to transformation and plating, deduplicated by product identity. + +### Level-2 route optimization + +Level-2 assembly expects a `ModuleDefinition` with engineered regions as components. If `region_order` is supplied, it is a hard constraint. If no order is supplied, the planner searches possible engineered-region orders up to the default exhaustive bound of 4 regions. + +Primary objective: + +```text +minimize number of new level-1 plasmids required +``` + +Tie-breakers: + +1. Respect hard constraints. +2. Prefer existing collection plasmids over generated/planned plasmids. +3. Prefer higher material state. +4. Prefer fewer total assemblies. +5. Tie-break by stable ordered SBOL identities. + +The detailed report should include the selected route and top 3 rejected alternatives. + +### Level-1 route optimization + +Level-1 assembly uses SBOL order when unambiguous. If SBOL order differs from promoter/RBS/CDS/terminator, warn the user but respect SBOL order. If order is ambiguous, enforce canonical promoter/RBS/CDS/terminator order. + +Primary objective: + +```text +minimize number of new domestications required +``` + +Tie-breakers mirror level-2 route selection. + +### Domestication + +Domestication produces a domesticated plasmid as the primary product. Edited inserts and internal BsaI-site edits are provenance/artifacts, not primary products. + +Compiler-only mode may propose sequence edits and record them. Protocol/execution mode must block unless the relevant process is approved. + +### Reporting + +Every `FullBuildResult` includes a lightweight `BuildSummary`. A comprehensive `BuildReport` is generated only when `options.reporting.include_detailed_report=True`. + +The detailed report should include: + +- status +- executive summary +- stage sections +- selected routes +- rejected alternatives +- missing inputs +- required approvals +- warnings +- next immediate actions +- full dependency chain +- graph summary + +## Success criteria + +V1 is successful when: + +- The new architecture exists with the target package boundaries. +- `BuildCompiler.plan(...)` classifies designs and expands valid combinatorial variants. +- `BuildCompiler.execute(...)` runs a bounded dependency loop with mocked/stubbed stages. +- Level-1 assembly is ported behind the new `AssemblyService` interface. +- Level-1 and level-2 route optimizers return selected routes plus structured blockers. +- Domestication reports sequence-edit approvals and missing backbones correctly. +- Compiler-only mode returns SBOL artifacts and PUDU-compatible JSON intermediates without PUDU/Opentrons. +- Transform/plating chaining deduplicates by product identity. +- `FullBuildResult.summary` always exists and `FullBuildResult.report` is opt-in. +- Core tests run under pytest + Ruff without optional automation dependencies. + +## Future considerations + +- DNA extraction as a real material-state transition. +- Larger level-2 search strategies beyond exhaustive order enumeration. +- Variable-length level-1 constructs. +- Persistent approval files. +- Rich SynBioSuite UI integration for route alternatives, build graph, and approvals. +- Stronger SBOL validation and provenance visualization. diff --git a/README.md b/README.md index ddaaa1a..d409123 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,269 @@ # BuildCompiler - BuildCompiler is an open-source tool that bridges the Design and Build stages of the DBTL cycle by compiling SBOL-encoded genetic designs into executable DNA assembly and transformation workflows. -It was developed to support build functionality in comand line and cloud workflows in [SynBioSuite](https://synbiosuite.org), based off the [SBOL Best Practices](https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011/). +BuildCompiler is a Python compiler pipeline for synthetic biology build planning. It takes abstract SBOL designs and indexed biological inventory, then produces an executable build plan across domestication, MoClo assembly level 1, MoClo assembly level 2, transformation, and plating. -BuildCompiler light logo -BuildCompiler night logo +This repository is being refactored around a clean architecture. The existing codebase is useful as working evidence, especially the level-1 assembly path and SBOL digestion/ligation behavior, but the new implementation should not preserve old APIs, old import paths, or legacy module boundaries except the root package name `buildcompiler`. -![PyPI - Version](https://img.shields.io/pypi/v/sbol2build) -[![Documentation Status](https://readthedocs.org/projects/sbol2build/badge/?version=latest)](https://sbol2build.readthedocs.io/en/latest/?badge=latest) -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sbol2build) -![PyPI - License](https://img.shields.io/pypi/l/sbol2build) -![gh-action badge](https://github.com/MyersResearchGroup/sbol2build/workflows/Python%20package/badge.svg) +## Why this exists -## Installing BuildCompiler: -```pip install buildcompiler``` +Designing a genetic construct is easier than building it in the lab. BuildCompiler should bridge that gap by compiling: -## Documentation +```text +abstract SBOL design + inventory -> build plan -> SBOL build artifacts -> PUDU JSON -> optional manual/OT-2 protocols +``` - Please visit the documentation with API reference and tutorials at Read the Docs: [sbol2build.rtfd.io](https://sbol2build.readthedocs.io) +The compiler should answer: -## Environment Setup +- Can this design be built from current inventory? +- Which plasmids, backbones, and reagents are required? +- Which missing engineered regions need level-1 assembly? +- Which missing promoter/RBS/CDS/terminator parts need domestication? +- Which route minimizes new build work? +- Which transformations and plating layouts follow from successful products? +- What actions should a user take next when a build is blocked? -If you are interested in contributing to **BuildCompiler**, please set up your local development environment with the same tools used in CI and linting. +## Core capabilities for v1 -### 1. Install [uv](https://docs.astral.sh/uv/) +- Classify SBOL abstract designs into level-2, level-1, domestication, or unsupported work. +- Plan and execute a bounded full-build dependency loop. +- Support partial success: build what can be built, report what is missing, and retry after generated products are indexed. +- Optimize level-2 routes by searching feasible engineered-region orders and minimizing new level-1 plasmids. +- Optimize level-1 routes by minimizing new domestications. +- Produce cumulative and per-stage SBOL build artifacts. +- Produce in-memory PUDU-compatible JSON intermediates in compiler-only mode. +- Chain successful assembly/domestication products to transformation and plating, deduplicated by product identity. +- Return structured statuses, missing inputs, required approvals, warnings, summaries, and optional detailed reports. +- Keep PUDU protocol generation and Opentrons simulation optional. -`uv` manages all Python dependencies (including dev tools) with a lockfile for reproducibility. +## Non-goals for v1 -#### Linux/Bash -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh +- Do not preserve legacy APIs or compatibility wrappers. +- Do not implement DNA extraction as a real stage; reserve `extracted` as a future material state. +- Do not run PUDU or Opentrons simulation by default. +- Do not make the build graph the scheduler in v1; it is reporting-only. +- Do not support variable-length level-1 constructs in v1. +- Do not silently approve sequence edits, reagent purchase, large combinatorial expansion, or large level-2 order search. + +## Architecture at a glance + +BuildCompiler should be organized as a compiler pipeline: + +```text +api -> planning -> execution -> stages -> sbol/inventory/adapters -> reporting ``` -#### Mac OSX with Homebrew -```bash -brew install uv + +Recommended package layout: + +```text +src/buildcompiler/ + __init__.py + + api/ + __init__.py + compiler.py + options.py + + domain/ + __init__.py + build_request.py + build_result.py + missing_input.py + material_state.py + plasmid.py + reagent.py + design.py + approvals.py + warnings.py + + planning/ + __init__.py + classifier.py + combinatorial.py + full_build_planner.py + validation.py + + execution/ + __init__.py + context.py + full_build_executor.py + worklist.py + stage_runner.py + indexing.py + + stages/ + __init__.py + domestication.py + assembly_lvl1.py + assembly_lvl2.py + transformation.py + plating.py + + sbol/ + __init__.py + assembly.py + domestication.py + transformation.py + documents.py + identities.py + resolver.py + validation.py + constants.py + + inventory/ + __init__.py + synbiohub.py + collection_indexer.py + plasmid_index.py + backbone_index.py + reagent_index.py + product_index.py + compatibility.py + selector.py + + adapters/ + __init__.py + pudu/ + __init__.py + assembly_json.py + transformation_json.py + plating_json.py + protocol_generation.py + opentrons/ + __init__.py + simulation.py + + reporting/ + __init__.py + build_graph.py + summaries.py + reports.py + serialization.py + + errors.py + logging.py +``` + +## Public API target + +Keep the import root `buildcompiler`. + +Primary usage should be stateful and object-based: + +```python +from buildcompiler.api import BuildCompiler + +compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) + +plan = compiler.plan(abstract_designs) +result = compiler.execute(plan) +``` + +A convenience wrapper may exist: + +```python +from buildcompiler.api import full_build + +result = full_build( + abstract_designs=abstract_designs, + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) ``` -### 2. Sync dependencies + +`BuildCompiler.__init__` should stay lightweight and dependency-injected. Automatic SynBioHub collection indexing belongs in `BuildCompiler.from_synbiohub(...)`. + +## Local development + +Recommended local workflow: + ```bash uv sync --all-groups +uv run ruff check . +uv run ruff format --check . +uv run pytest ``` -This will create a virtual environment with the dependiencies. Activate using: + +If `uv` is not available, use a normal virtual environment and install editable dependencies: + ```bash +python -m venv .venv source .venv/bin/activate +python -m pip install -U pip +python -m pip install -e '.[test]' +python -m pip install ruff +ruff check . +ruff format --check . +pytest +``` + +Automation-specific tests should be optional: + +```bash +python -m pip install -e '.[automation,test]' +pytest tests/automation +``` + +## Container workflow + +A Docker Compose workflow is recommended for reliable Codex and contributor development, but it does not need to block the first implementation PR. + +Target commands after Docker support exists: + +```bash +docker compose build +docker compose run --rm app ruff check . +docker compose run --rm app ruff format --check . +docker compose run --rm app pytest ``` -### 3. Install pre-commit hooks -We use pre-commit to automatically run the Ruff linter before every commit. -Install and enable the hooks with: +Core CI should not require PUDU or Opentrons. Those dependencies are optional and should live behind optional test jobs or manual workflows. + +## Testing and quality checks + +Default CI should run: + ```bash -uv run pre-commit install +ruff check . +ruff format --check . +pytest tests/unit tests/stages tests/integration ``` +Testing priorities: + +1. Domain dataclasses and status semantics. +2. Planning and design classification. +3. Inventory indexing and compatibility selection. +4. Level-1 and level-2 route optimizers. +5. Domestication sequence-edit approval behavior. +6. Full-build bounded retry loop with mocked stages. +7. SBOL assembly service port using existing fixtures. +8. Transformation and plating deduplication. +9. Summary/report/graph generation. +10. Optional PUDU/Opentrons adapter smoke tests. + +## How ChatGPT and Codex should use these docs + +- `PRODUCT.md` defines the product intent, v1 scope, and non-goals. +- `ARCHITECTURE.md` defines module boundaries and implementation contracts. +- `AGENT.md` defines how Codex should take tasks, report progress, and escalate decisions. +- `ADR-001.md` records the clean-architecture rewrite decision and its tradeoffs. +Codex should treat these files as the source of truth for implementation unless the user or ChatGPT explicitly revises them. + +#### Running tests locally: +Run these bash commands to establish your SynBioHub account for collection access. These are saved in GitHub secrets for the automated test suite. + +`export SBH_USERNAME=your_username` + +`export SBH_PASSWORD=your_password` + +Then run the tests with: -#### Running tests: `uv run python -m unittest discover -s tests` + diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md new file mode 100644 index 0000000..64647c1 --- /dev/null +++ b/RELEASE_CHECKLIST.md @@ -0,0 +1,11 @@ +# Release checklist + +- [ ] Core CI (`pytest tests/unit tests/stages tests/integration`) is green. +- [ ] `ruff check .` is green. +- [ ] `ruff format --check .` is green. +- [ ] Happy-path integration fixture passes (or has explicit xfail with blocking issue). +- [ ] Missing-lvl1-then-domestication integration fixture passes (or has explicit xfail with blocking issue). +- [ ] Optional automation tests are documented and kept manual. +- [ ] Core tests do not import optional automation dependencies. +- [ ] README testing commands remain accurate. +- [ ] Known upstream blockers are documented with issue references. diff --git a/SBOL2Build.ipynb b/SBOL2Build.ipynb deleted file mode 100644 index b46e23f..0000000 --- a/SBOL2Build.ipynb +++ /dev/null @@ -1,2057 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# SBOL2Build" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sbol2\n", - "import tyto\n", - "import re\n", - "from Bio import Restriction\n", - "from Bio.Seq import Seq\n", - "from pydna.dseqrecord import Dseqrecord\n", - "from itertools import product\n", - "from typing import Dict, Iterable, List, Union, Optional, Tuple\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sbol2.Config.setHomespace('https://SBOL2Build.org')" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [], - "source": [ - "# this document is used by all the next \n", - "doc = sbol2.Document()\n", - "doc.addNamespace('http://SBOL2Build#', 'SBOL2Build')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Restriction enzyme" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target function\n", - "\n", - "def ed_restriction_enzyme(name:str, **kwargs) -> sbol3.ExternallyDefined:\n", - " \"\"\"Creates an ExternallyDefined Restriction Enzyme Component from rebase.\n", - "\n", - " :param name: Name of the SBOL ExternallyDefined, used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI'\n", - " :param kwargs: Keyword arguments of any other ExternallyDefined attribute.\n", - " :return: An ExternallyDefined object.\n", - " \"\"\"\n", - " check_enzyme = Restriction.__dict__[name]\n", - " definition=f'http://rebase.neb.com/rebase/enz/{name}.html' # TODO: replace with getting the URI from Enzyme when REBASE identifiers become available in biopython 1.8\n", - " return sbol3.ExternallyDefined([sbol3.SBO_PROTEIN], definition=definition, name=name, **kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL 2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def rebase_restriction_enzyme(name:str, **kwargs) -> sbol2.ComponentDefinition:\n", - " \"\"\"Creates an ComponentDefinition Restriction Enzyme Component from rebase.\n", - "\n", - " :param name: Name of the SBOL ExternallyDefined, used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI'\n", - " :param kwargs: Keyword arguments of any other ComponentDefinition attribute.\n", - " :return: A ComponentDefinition object.\n", - " \"\"\"\n", - " check_enzyme = Restriction.__dict__[name]\n", - " definition=f'http://rebase.neb.com/rebase/enz/{name}.html' # TODO: replace with getting the URI from Enzyme when REBASE identifiers become available in biopython 1.8\n", - " cd = sbol2.ComponentDefinition(name)\n", - " cd.types = sbol2.BIOPAX_PROTEIN\n", - " cd.name = name\n", - " cd.roles = []\n", - " cd.wasDerivedFrom = definition\n", - " cd.description = f'Restriction enzyme {name} from REBASE.'\n", - " return cd\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "bsai = rebase_restriction_enzyme(name=\"BsaI\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "BsaI\n" - ] - } - ], - "source": [ - "bsai.wasDerivedFrom\n", - "print(bsai.name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# DNA ComponentDefinition with Sequence" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'sbol3' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[11], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m#target helper function\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdna_component_with_sequence\u001b[39m(identity: \u001b[38;5;28mstr\u001b[39m, sequence: \u001b[38;5;28mstr\u001b[39m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tuple[sbol3\u001b[38;5;241m.\u001b[39mComponent, sbol3\u001b[38;5;241m.\u001b[39mSequence]:\n\u001b[1;32m 3\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Creates a DNA Component and its Sequence.\u001b[39;00m\n\u001b[1;32m 4\u001b[0m \n\u001b[1;32m 5\u001b[0m \u001b[38;5;124;03m :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;124;03m :return: A tuple of Component and Sequence.\u001b[39;00m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 10\u001b[0m comp_seq \u001b[38;5;241m=\u001b[39m sbol3\u001b[38;5;241m.\u001b[39mSequence(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00midentity\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_seq\u001b[39m\u001b[38;5;124m'\u001b[39m, elements\u001b[38;5;241m=\u001b[39msequence, encoding\u001b[38;5;241m=\u001b[39msbol3\u001b[38;5;241m.\u001b[39mIUPAC_DNA_ENCODING)\n", - "\u001b[0;31mNameError\u001b[0m: name 'sbol3' is not defined" - ] - } - ], - "source": [ - "#target helper function\n", - "def dna_component_with_sequence(identity: str, sequence: str, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Creates a DNA Component and its Sequence.\n", - "\n", - " :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sequence: The DNA sequence of the Component encoded in IUPAC.\n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of Component and Sequence.\n", - " \"\"\"\n", - " comp_seq = sbol3.Sequence(f'{identity}_seq', elements=sequence, encoding=sbol3.IUPAC_DNA_ENCODING)\n", - " dna_comp = sbol3.Component(identity, sbol3.SBO_DNA, sequences=[comp_seq], **kwargs)\n", - " return dna_comp, comp_seq\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def dna_componentdefinition_with_sequence2(identity: str, sequence: str, **kwargs) -> Tuple[sbol2.ComponentDefinition, sbol2.Sequence]:\n", - " \"\"\"Creates a DNA ComponentDefinition and its Sequence.\n", - "\n", - " :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sequence: The DNA sequence of the Component encoded in IUPAC.\n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of ComponentDefinition and Sequence.\n", - " \"\"\"\n", - " comp_seq = sbol2.Sequence(f'{identity}_seq', elements=sequence, encoding=sbol2.SBOL_ENCODING_IUPAC)\n", - " dna_comp = sbol2.ComponentDefinition(identity, sbol2.BIOPAX_DNA, **kwargs)\n", - " dna_comp.sequences = [comp_seq]\n", - "\n", - " return dna_comp, comp_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Target Part in Backbone from SBOL (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target func\n", - "\n", - "def part_in_backbone_from_sbol(identity: Union[str, None], sbol_comp: sbol3.Component, part_location: List[int], part_roles:List[str], fusion_site_length:int, linear:bool=False, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Restructures a non-hierarchical plasmid Component to follow the part-in-backbone pattern following BP011.\n", - " It overwrites the SBOL3 Component provided. \n", - " A part inserted into a backbone is represented by a Component that includes both the part insert \n", - " as a feature that is a SubComponent and the backbone as another SubComponent.\n", - " For more information about BP011 visit https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011 \n", - "\n", - " :param identity: The identity of the Component, is its a String it build a new SBOL Component, if None it adds on top of the input. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sbol_comp: The SBOL3 Component that will be used to create the part in backbone Component and Sequence.\n", - " :param part_location: List of 2 integers that indicates the start and the end of the unitary part. Note that the index of the first location is 1, as is typical practice in biology, rather than 0, as is typical practice in computer science.\n", - " :param part_roles: List of strings that indicates the roles to add on the part.\n", - " :param fusion_site_length: Integer of the length of the fusion sites (eg. BsaI fusion site lenght is 4, SapI fusion site lenght is 3)\n", - " :param linear: Boolean than indicates if the backbone is linear, by default it is seted to Flase which means that it has a circular topology. \n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of Component and Sequence.\n", - " \"\"\"\n", - " if len(part_location) != 2:\n", - " raise ValueError('The part_location only accepts 2 int values in a list.')\n", - " if len(sbol_comp.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(sbol_comp.sequences)} sequences')\n", - " sequence = sbol_comp.sequences[0].lookup().elements\n", - " if identity == None:\n", - " part_in_backbone_component = sbol_comp \n", - " part_in_backbone_seq = sbol_comp.sequences[0]\n", - " else:\n", - " part_in_backbone_component, part_in_backbone_seq = dna_component_with_sequence(identity, sequence, **kwargs)\n", - " part_in_backbone_component.roles.append(sbol3.SO_DOUBLE_STRANDED)\n", - " for part_role in part_roles: \n", - " part_in_backbone_component.roles.append(part_role) \n", - " # creating part feature \n", - " part_location_comp = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[0], end=part_location[1])\n", - " #TODO: add the option of fusion sites to be of different lenghts\n", - " insertion_site_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[0], end=part_location[0]+fusion_site_length, order=1)\n", - " insertion_site_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=part_location[1], order=3)\n", - " part_sequence_feature = sbol3.SequenceFeature(locations=[part_location_comp], roles=part_roles)\n", - " part_sequence_feature.roles.append(tyto.SO.engineered_insert)\n", - " insertion_sites_feature = sbol3.SequenceFeature(locations=[insertion_site_location1, insertion_site_location2], roles=[tyto.SO.insertion_site])\n", - " #TODO: infer topology from the input\n", - " if linear:\n", - " part_in_backbone_component.types.append(sbol3.SO_LINEAR)\n", - " part_in_backbone_component.roles.append(sbol3.SO_ENGINEERED_REGION)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=1, end=part_location[0]+fusion_site_length-1, order=1)\n", - " open_backbone_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=len(sequence), order=3)\n", - " open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])\n", - " else: \n", - " part_in_backbone_component.types.append(sbol3.SO_CIRCULAR)\n", - " part_in_backbone_component.roles.append(tyto.SO.plasmid_vector)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=1, end=part_location[0]+fusion_site_length-1, order=2)\n", - " open_backbone_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=len(sequence), order=1)\n", - " open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])\n", - " part_in_backbone_component.features.append(part_sequence_feature)\n", - " part_in_backbone_component.features.append(insertion_sites_feature)\n", - " part_in_backbone_component.features.append(open_backbone_feature)\n", - " backbone_dropout_meets = sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=part_sequence_feature, object=open_backbone_feature)\n", - " part_in_backbone_component.constraints.append(backbone_dropout_meets)\n", - " #TODO: Add a branch to create a component without overwriting the WHOLE input component\n", - " #removing repeated types and roles\n", - " part_in_backbone_component.types = set(part_in_backbone_component.types)\n", - " part_in_backbone_component.roles = set(part_in_backbone_component.roles)\n", - " return part_in_backbone_component, part_in_backbone_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 Part In Backbone Implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "def part_in_backbone_from_sbol2(identity: Union[str, None], sbol_comp: sbol2.ModuleDefinition, part_location: List[int], part_roles:List[str], fusion_site_length:int, linear:bool=False, **kwargs) -> Tuple[sbol2.ComponentDefinition, sbol2.Sequence]:\n", - " if len(part_location) != 2:\n", - " raise ValueError('The part_location only accepts 2 int values in a list.')\n", - " if len(sbol_comp.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(sbol_comp.sequences)} sequences')\n", - " sequence = doc.find(sbol_comp.sequences[0]).elements\n", - " if identity == None:\n", - " part_in_backbone_component = sbol_comp \n", - " part_in_backbone_seq = doc.find(sbol_comp.sequences[0]).elements\n", - " part_in_backbone_component.sequences = [part_in_backbone_seq]\n", - " else:\n", - " part_in_backbone_component, part_in_backbone_seq = dna_componentdefinition_with_sequence2(identity, sequence, **kwargs)\n", - " # double stranded\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000985')\n", - " for part_role in part_roles: \n", - " part_in_backbone_component.addRole(part_role)\n", - "\n", - " # creating part annotation \n", - " part_location_comp = sbol2.Range( start=part_location[0], end=part_location[1])\n", - " insertion_site_location1 = sbol2.Range( uri=\"insertloc1\", start=part_location[0], end=part_location[0]+fusion_site_length) #order 1\n", - " insertion_site_location2 = sbol2.Range( uri=\"insertloc2\", start=part_location[1]-fusion_site_length, end=part_location[1]) #order 3\n", - "\n", - " part_sequence_annotation = sbol2.SequenceAnnotation('part_sequence_annotation')\n", - " part_sequence_annotation.roles = part_roles\n", - " part_sequence_annotation.locations.add(part_location_comp)\n", - "\n", - " part_sequence_annotation.addRole(tyto.SO.engineered_insert)\n", - " insertion_sites_annotation = sbol2.SequenceAnnotation('insertion_sites_annotation')\n", - "\n", - " insertion_sites_annotation.locations.add(insertion_site_location1)\n", - " insertion_sites_annotation.locations.add(insertion_site_location2)\n", - " \n", - " insertion_sites_annotation.roles = [tyto.SO.insertion_site]\n", - " if linear:\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000987') #linear\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000804') #engineered region\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol2.Range(start=1, end=part_location[0]+fusion_site_length-1) #order 1\n", - " open_backbone_location2 = sbol2.Range(start=part_location[1]-fusion_site_length, end=len(sequence)) #order 3\n", - " open_backbone_annotation = sbol2.SequenceAnnotation(locations=[open_backbone_location1, open_backbone_location2])\n", - " else: \n", - " part_in_backbone_component.addRole('http://identifiers.or/SO:0000988') #circular\n", - " part_in_backbone_component.addRole(tyto.SO.plasmid_vector)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol2.Range( uri=\"backboneloc1\", start=1, end=part_location[0]+fusion_site_length-1 ) #order 2\n", - " open_backbone_location2 = sbol2.Range( uri=\"backboneloc2\", start=part_location[1]-fusion_site_length, end=len(sequence)) #order 1\n", - " open_backbone_annotation = sbol2.SequenceAnnotation('open_backbone_annotation')\n", - " open_backbone_annotation.locations.add(open_backbone_location1)\n", - " open_backbone_annotation.locations.add(open_backbone_location2)\n", - " \n", - " part_in_backbone_component.sequenceAnnotations.add(part_sequence_annotation)\n", - " part_in_backbone_component.sequenceAnnotations.add(insertion_sites_annotation)\n", - " part_in_backbone_component.sequenceAnnotations.add(open_backbone_annotation) \n", - " # use sequenceconstrait with precedes\n", - " # backbone_dropout_meets = sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=part_sequence_annotation, object=open_backbone_annotation) #????\n", - " backbone_dropout_meets = sbol2.sequenceconstraint.SequenceConstraint(uri='backbone_dropout_meets', restriction=sbol2.SBOL_RESTRICTION_PRECEDES) #might need to add uri as param 2\n", - " backbone_dropout_meets.subject = part_sequence_annotation\n", - " backbone_dropout_meets.object = open_backbone_annotation\n", - " \n", - " part_in_backbone_component.sequenceConstraints.add(backbone_dropout_meets)\n", - " #TODO: Add a branch to create a component without overwriting the WHOLE input component\n", - " #removing repeated types and roles\n", - " part_in_backbone_component.types = set(part_in_backbone_component.types)\n", - " part_in_backbone_component.roles = set(part_in_backbone_component.roles)\n", - " return part_in_backbone_component, part_in_backbone_seq" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# benchling plasmid test\n", - "doc = sbol2.Document()\n", - "benchling_comp = sbol2.ComponentDefinition('benchling_comp', sbol2.BIOPAX_DNA)\n", - "\n", - "benchSeq = sbol2.Sequence('benchSeq', 'tcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcgGGAGtttacagctagctcagtcctaggtattatgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtct', sbol2.SBOL_ENCODING_IUPAC)\n", - "\n", - "doc.addComponentDefinition(benchling_comp)\n", - "doc.addSequence(benchSeq)\n", - "benchling_comp.sequences = [benchSeq] \n", - "\n", - "resultComponent, resultSequence = part_in_backbone_from_sbol2(identity=\"benchling_comp\", sbol_comp=benchling_comp, part_location=[531, 602], part_roles=[], fusion_site_length=6)\n", - "\n", - "doc2 = sbol2.Document()\n", - "\n", - "doc2.addComponentDefinition(resultComponent)\n", - "doc2.addComponentDefinition(resultSequence)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Invalid. sbol-11403:\\x00 Strong Validation Error:\\x00 The Component referenced by the subject property of a SequenceConstraint MUST be contained by the ComponentDefinition that contains the SequenceConstraint. \\x00Reference: SBOL Version 2.3.0 Section 7.7.6 on page 36 :\\x00 http://examples.org/ComponentDefinition/benchling_comp/backbone_dropout_meets/1\\x00 Validation failed.'" - ] - }, - "execution_count": 156, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "doc2.write('example1.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize SBOL2 Document\n", - "sbol2.Config.setOption('sbol_typed_uris', True)\n", - "doc = sbol2.Document()\n", - "\n", - "# Create a ComponentDefinition with a DNA sequence\n", - "component_def = sbol2.ComponentDefinition('example_component', sbol2.BIOPAX_DNA)\n", - "doc.addComponentDefinition(component_def)\n", - "\n", - "# Add a sequence to the ComponentDefinition\n", - "sequence = sbol2.Sequence('example_sequence', 'ATGCTGACTGCTAGCTGACTAGC', sbol2.SBOL_ENCODING_IUPAC)\n", - "doc.addSequence(sequence)\n", - "component_def.sequences = [sequence.identity] # Associate the sequence with the ComponentDefinition\n", - "\n", - "# Create a SequenceAnnotation\n", - "seq_annotation = sbol2.SequenceAnnotation('example_annotation')\n", - "range_location = sbol2.Range('new range', 1, 6) # Create a Range object\n", - "seq_annotation.locations.add(range_location) # Add it to the annotation\n", - "component_def.sequenceAnnotations.add(seq_annotation)\n", - "\n", - "# Optionally add a role (e.g., promoter, CDS) to describe the feature\n", - "seq_annotation.roles = ['http://identifiers.org/SO:0000167'] # Promoter role from Sequence Ontology\n", - "\n", - "# Save the SBOL2 document\n", - "doc.write('sequence_annotation_example.xml')\n", - "\n", - "print('SequenceAnnotation created successfully!')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "component_def = sbol2.ComponentDefinition('example_component', sbol2.BIOPAX_DNA)\n", - "sequence = sbol2.Sequence('example_sequence', 'ATGCTGACTGCTAGCTGACTAGC', sbol2.SBOL_ENCODING_IUPAC)\n", - "component_def.sequences = [sequence.identity] # Associate the sequence with the ComponentDefinition\n", - "\n", - "\n", - "anno_1 = sbol2.SequenceAnnotation('new_anno')\n", - "anno_2 = sbol2.SequenceAnnotation('new_anno_2')\n", - "\n", - "range_location = sbol2.Range('new range', 1, 6) # Create a Range object\n", - "anno_1.locations.add(range_location) # Add it to the annotation)\n", - "\n", - "component_def.sequenceAnnotations.add(anno_1)\n", - "component_def.sequenceAnnotations.add(anno_2)\n", - "\n", - "print(sbol2.SBOL_RESTRICTION_PRECEDES)\n", - "\n", - "# new_constraint = sbol2.SequenceConstraint(restriction=SBOL_RESTRICTION_PRECEDES, subject=anno_1, object=anno_2) #????\n", - "backbone_dropout_meets = sbol2.sequenceconstraint.SequenceConstraint (\n", - " 'backbone_dropout',\n", - " 'backbone_dropout_uri',\n", - " anno_1,\n", - " anno_2,\n", - " sbol2.SBOL_RESTRICTION_PRECEDES\n", - ")\n", - "\n", - "component_def.sequenceConstraints.add(backbone_dropout_meets)\n", - "\n", - "sbol2.DNA" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target func\n", - "class Assembly_plan_composite_in_backbone_single_enzyme():\n", - " \"\"\"Creates a Assembly Plan.\n", - " :param name: Name of the assembly plan Component.\n", - " :param parts_in_backbone: Parts in backbone to be assembled. \n", - " :param acceptor_backbone: Backbone in which parts are inserted on the assembly. \n", - " :param restriction_enzymes: Restriction enzyme with correct name from Bio.Restriction as Externally Defined.\n", - " :param document: SBOL Document where the assembly plan will be created.\n", - " :param linear: Boolean to inform if the reactant is linear.\n", - " :param circular: Boolean to inform if the reactant is circular.\n", - " :param **kwargs: Keyword arguments of any other Component attribute for the assembled part.\n", - " \"\"\"\n", - "\n", - " def __init__(self, name: str, parts_in_backbone: List[sbol3.Component], acceptor_backbone: sbol3.Component, restriction_enzyme: Union[str,sbol3.ExternallyDefined], document:sbol3.Document):\n", - " self.name = name\n", - " self.parts_in_backbone = parts_in_backbone\n", - " self.acceptor_backbone = acceptor_backbone\n", - " self.restriction_enzyme = restriction_enzyme\n", - " self.products = []\n", - " self.extracted_parts = []\n", - " self.document = document\n", - "\n", - " #create assembly plan\n", - " self.assembly_plan_component = sbol3.Component(identity=f'{self.name}_assembly_plan', types=sbol3.SBO_FUNCTIONAL_ENTITY)\n", - " self.document.add(self.assembly_plan_component)\n", - " self.composites = []\n", - "\n", - " def run(self):\n", - " self.assembly_plan_component.features.append(self.restriction_enzyme)\n", - " #extract parts\n", - " part_number = 1\n", - " for part_in_backbone in self.parts_in_backbone:\n", - " part_comp, part_seq = digestion(reactant=part_in_backbone,restriction_enzymes=[self.restriction_enzyme], assembly_plan=self.assembly_plan_component, name=f'part_{part_number}_{part_in_backbone.display_id}')\n", - " self.document.add([part_comp, part_seq])\n", - " self.extracted_parts.append(part_comp)\n", - " part_number += 1\n", - " #extract backbone (should be the same?)\n", - " backbone_comp, backbone_seq = digestion(reactant=self.acceptor_backbone,restriction_enzymes=[self.restriction_enzyme], assembly_plan=self.assembly_plan_component, name=f'part_{part_number}')\n", - " self.document.add([backbone_comp, backbone_seq])\n", - " self.extracted_parts.append(backbone_comp)\n", - " \n", - " #create composite part from extracted parts\n", - " composites_list = ligation(reactants=self.extracted_parts, assembly_plan=self.assembly_plan_component)\n", - " for composite in composites_list:\n", - " composite[0].generated_by.append(self.assembly_plan_component) #\n", - " self.composites.append(composite)\n", - " self.products.append(composite)\n", - " self.document.add(composite)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Digestion" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3 (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def digestion(reactant:sbol3.Component, restriction_enzymes:List[sbol3.ExternallyDefined], assembly_plan:sbol3.Component, **kwargs)-> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Digests a Component using the provided restriction enzymes and creates a product Component and a digestion Interaction.\n", - " The product Component is assumed to be the insert for parts in backbone and the backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL Component, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used Externally Defined.\n", - " :return: A tuple of Component and Interaction.\n", - " \"\"\"\n", - " if sbol3.SBO_DNA not in reactant.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types founded {reactant.types}.')\n", - " if len(reactant.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " restriction_enzymes_pydna=[] \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - " modifier_participation = sbol3.Participation(roles=[sbol3.SBO_MODIFIER], participant=re)\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant.sequences[0].lookup().elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, linear=linear, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna)\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3:\n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - " \n", - " # Extracting roles from features\n", - " reactant_features_roles = []\n", - " for f in reactant.features:\n", - " for r in f.roles:\n", - " reactant_features_roles.append(r)\n", - " # if part\n", - " if any(n==tyto.SO.engineered_insert for n in reactant_features_roles):\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = part_extract.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = part_extract.seq.three_prime_end()\n", - " \n", - " product_sequence = str(part_extract.seq)\n", - " prod_component_definition, prod_seq = dna_component_with_sequence(identity=f'{reactant.name}_part_extract', sequence=product_sequence, **kwargs) #str(product_sequence))\n", - " # add sticky ends features\n", - " five_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=1, end=len(product_5_prime_ss_end), order=1)\n", - " three_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence), order=3)\n", - " fusion_sites_feature = sbol3.SequenceFeature(locations=[five_prime_fusion_site_location, three_prime_fusion_site_location], roles=[tyto.SO.insertion_site])\n", - " prod_component_definition.roles.append(tyto.SO.engineered_insert) \n", - " prod_component_definition.features.append(fusion_sites_feature)\n", - "\n", - " # if backbone\n", - " elif any(n==tyto.SO.deletion for n in reactant_features_roles):\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = backbone.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = backbone.seq.three_prime_end()\n", - " product_sequence = str(backbone.seq)\n", - " prod_component_definition, prod_seq = dna_component_with_sequence(identity=f'{reactant.name}_backbone', sequence=product_sequence, **kwargs) #str(product_sequence))\n", - " # add sticky ends features\n", - " five_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=1, end=len(product_5_prime_ss_end), order=1)\n", - " three_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence), order=3)\n", - " fusion_sites_feature = sbol3.SequenceFeature(locations=[five_prime_fusion_site_location, three_prime_fusion_site_location], roles=[tyto.SO.insertion_site])\n", - " prod_component_definition.roles.append(tyto.SO.plasmid_vector)\n", - " prod_component_definition.features.append(fusion_sites_feature)\n", - "\n", - " else: raise NotImplementedError('The reactant has no valid roles')\n", - "\n", - " #Add reference to part in backbone\n", - " reactant_subcomponent = sbol3.SubComponent(reactant)\n", - " prod_component_definition.features.append(reactant_subcomponent)\n", - " # Create reactant Participation.\n", - " react_subcomp = sbol3.SubComponent(reactant)\n", - " assembly_plan.features.append(react_subcomp)\n", - " reactant_participation = sbol3.Participation(roles=[sbol3.SBO_REACTANT], participant=react_subcomp)\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_subcomp = sbol3.SubComponent(prod_comp)\n", - " assembly_plan.features.append(prod_subcomp)\n", - " product_participation = sbol3.Participation(roles=[sbol3.SBO_PRODUCT], participant=prod_subcomp)\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol3.Interaction(types=[tyto.SBO.cleavage], participations=participations)\n", - " assembly_plan.interactions.append(interaction)\n", - " \n", - " return prod_comp, prod_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# SBOL2 implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Is Ciruclar" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# helper function\n", - "def is_circular(obj: sbol2.ComponentDefinition) -> bool:\n", - " \"\"\"Check if an SBOL Component or Feature is circular.\n", - " :param obj: design to be checked\n", - " :return: true if circular\n", - " \"\"\" \n", - " return any(n==sbol2.SO_CIRCULAR for n in obj.types)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All tests passed!\n" - ] - } - ], - "source": [ - "import unittest\n", - "\n", - "cd1 = sbol2.ComponentDefinition(\"circular_cd\")\n", - "cd1.types = [sbol2.SO_CIRCULAR]\n", - "assert is_circular(cd1), \"Circular ComponentDefinition should return True\"\n", - "\n", - "cd2 = sbol2.ComponentDefinition(\"non_circular_cd\")\n", - "cd2.types = [sbol2.BIOPAX_DNA]\n", - "assert not is_circular(cd2), \"Non-circular ComponentDefinition should return False\"\n", - "\n", - "cd3 = sbol2.ComponentDefinition(\"empty_types\")\n", - "assert not is_circular(cd3), \"Empty types list should return False\"\n", - "\n", - "print(\"All tests passed!\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Part Digestion" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "def part_digestion(reactant:sbol2.ModuleDefinition, restriction_enzymes:List[sbol2.ComponentDefinition], assembly_plan:sbol2.ModuleDefinition, document: sbol2.Document, **kwargs)-> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.ModuleDefinition]:\n", - " \"\"\"Digests a ModuleDefinition using the provided restriction enzymes and creates a product ComponentDefinition and a digestion Interaction.\n", - " The product ComponentDefinition is assumed to be the insert for parts in backbone and the open backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL ModuleDefinition, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used ComponentDefinition.\n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document to be used to extract referenced objects.\n", - " :return: A list of tuples of [ComponentDefinition, Sequence], and an assemblyplan ModuleDefinition.\n", - " \"\"\"\n", - " # extract component definition from module\n", - " reactant_displayId = reactant.functionalComponents[0].displayId\n", - " reactant_def_URI = reactant.functionalComponents[0].definition\n", - " reactant_component_definition = document.getComponentDefinition(reactant_def_URI)\n", - "\n", - " if sbol2.BIOPAX_DNA not in reactant_component_definition.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types found {reactant.types}.')\n", - " if len(reactant_component_definition.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " extracts_list=[]\n", - " restriction_enzymes_pydna=[] \n", - " \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - "\n", - " enzyme_component = sbol2.FunctionalComponent(uri=f\"{re.name}_enzyme\")\n", - " enzyme_component.definition = re\n", - " enzyme_component.displayID = f\"{re.name}_enzyme\"\n", - " enzyme_in_module = False\n", - "\n", - " for comp in assembly_plan.functionalComponents:\n", - " if (comp.displayId == enzyme_component.displayID): \n", - " enzyme_component = comp\n", - " enzyme_in_module = True\n", - "\n", - " if not enzyme_in_module:\n", - " assembly_plan.functionalComponents.add(enzyme_component)\n", - " \n", - " modifier_participation = sbol2.Participation(uri='restriction')\n", - " modifier_participation.participant = enzyme_component\n", - " modifier_participation.roles = ['http://identifiers.org/biomodels.sbo/SBO:0000019']\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant_component_definition):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant_component_definition.sequences[0]\n", - " reactant_seq = document.getSequence(reactant_seq).elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna) #TODO see if ds_reactant.cut is working, causing problems downstream\n", - "\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3: \n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - "\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = part_extract.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = part_extract.seq.three_prime_end()\n", - " product_sequence = str(part_extract.seq)\n", - " prod_component_definition, prod_seq = dna_componentdefinition_with_sequence2(identity=f'{reactant.functionalComponents[0].displayId}_extracted_part', sequence=product_sequence, **kwargs)\n", - " prod_component_definition.wasDerivedFrom = reactant_component_definition.identity\n", - " extracts_list.append((prod_component_definition, prod_seq))\n", - "\n", - " # five prime overhang\n", - " five_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_five_prime_oh\") # TODO: ensure circular type is preserved for sbh visualization\n", - " five_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001932')\n", - " five_prime_oh_location = sbol2.Range(uri=\"five_prime_oh_location\", start=1, end=len(product_5_prime_ss_end))\n", - " five_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_five_prime_oh_component\")\n", - " five_prime_oh_component.definition = five_prime_oh_definition\n", - " five_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"five_prime_overhang\")\n", - " five_prime_overhang_annotation.locations.add(five_prime_oh_location)\n", - "\n", - " # extracted part => point straight to part from sbolcanvas\n", - " part_location = sbol2.Range(uri=f\"{reactant_displayId}_part_location\", start=len(product_5_prime_ss_end)+1, end=len(product_sequence)-len(product_3_prime_ss_end))\n", - " part_extract_annotation = sbol2.SequenceAnnotation(uri=f\"{reactant_displayId}_part\")\n", - " part_extract_annotation.locations.add(part_location)\n", - "\n", - " # three prime overhang\n", - " three_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_three_prime_oh\")\n", - " three_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001933')\n", - " three_prime_oh_location = sbol2.Range(uri=\"three_prime_oh_location\", start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence)) \n", - " three_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_three_prime_oh_component\")\n", - " three_prime_oh_component.definition = three_prime_oh_definition\n", - " three_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"three_prime_overhang\")\n", - " three_prime_overhang_annotation.locations.add(three_prime_oh_location)\n", - " \n", - " # three_prime_overhang_annotation.addRole(tyto.SO.insertion_site) #TODO: do we need this, or change to three_prime_overhang_annotation.addRole('http://identifiers.org/SO:0000366')?\n", - "\n", - " anno_component_dict = {}\n", - "\n", - " prod_component_definition.components = [five_prime_oh_component, three_prime_oh_component]\n", - " three_prime_overhang_annotation.component = three_prime_oh_component\n", - " five_prime_overhang_annotation.component = five_prime_oh_component\n", - "\n", - " original_part_def_URI = \"\"\n", - "\n", - " #enccode ontologies of overhangs\n", - " for definition in document.componentDefinitions:\n", - " for seqURI in definition.sequences:\n", - " seq = document.getSequence(seqURI)\n", - " if seq.elements.lower() == Seq(product_3_prime_ss_end).reverse_complement():\n", - " three_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " three_prime_sequence = sbol2.Sequence(uri=f\"{three_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " three_prime_sequence.wasDerivedFrom = seq.identity\n", - " three_prime_oh_definition.sequences = [three_prime_sequence]\n", - " three_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - " \n", - " extracts_list.append((three_prime_oh_definition, three_prime_sequence))\n", - " extracts_list.append((definition, seq)) #add scars to list\n", - "\n", - " elif seq.elements.lower() == product_sequence[4:-4]: \n", - " original_part_def_URI = definition.identity\n", - " extracts_list.append((definition, seq))\n", - "\n", - " elif seq.elements.lower() == product_5_prime_ss_end:\n", - " five_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " five_prime_sequence = sbol2.Sequence(uri=f\"{five_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " five_prime_sequence.wasDerivedFrom = seq.identity\n", - " five_prime_oh_definition.sequences = [five_prime_sequence]\n", - " five_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - "\n", - " extracts_list.append((five_prime_oh_definition, five_prime_sequence))\n", - " extracts_list.append((definition, seq))\n", - "\n", - " #find + add original component to product def & annotation\n", - " for comp in reactant_component_definition.components: \n", - " if comp.definition == original_part_def_URI:\n", - " prod_component_definition.components.add(comp)\n", - " part_extract_annotation.component = comp\n", - "\n", - " prod_component_definition.sequenceAnnotations.add(three_prime_overhang_annotation)\n", - " prod_component_definition.sequenceAnnotations.add(five_prime_overhang_annotation)\n", - " prod_component_definition.sequenceAnnotations.add(part_extract_annotation)\n", - " prod_component_definition.addRole(tyto.SO.engineered_insert) \n", - " prod_component_definition.addType('http://identifiers.org/so/SO:0000987')\n", - "\n", - " #Add reference to part in backbone\n", - " reactant_component = sbol2.FunctionalComponent(uri=f\"{reactant_displayId}_reactant\")\n", - " reactant_component.definition = reactant_component_definition\n", - " assembly_plan.functionalComponents.add(reactant_component)\n", - "\n", - " # Create reactant Participation.\n", - " reactant_participation = sbol2.Participation(uri=f\"{reactant_displayId}_reactant\")\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_component = sbol2.FunctionalComponent(uri=f\"{reactant_displayId}_digestion_product\")\n", - " prod_component.definition = prod_component_definition\n", - " assembly_plan.functionalComponents.add(prod_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f\"{reactant_displayId}_product\")\n", - " product_participation.participant = prod_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{reactant_displayId}_digestion_interaction\", interaction_type='https://identifiers.org/biomodels.sbo/SBO:0000178')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - " \n", - " return extracts_list, assembly_plan" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_extracted_part_seq/1\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_B/1\n", - "https://sbolcanvas.org/Scar_B_sequence\n", - "https://sbolcanvas.org/J23101/1\n", - "https://sbolcanvas.org/J23101_sequence\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 117, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pro_in_bb_doc = sbol2.Document()\n", - "pro_in_bb_doc.read('tests/test_files/test_files/pro_in_bb.xml')\n", - "md = pro_in_bb_doc.getModuleDefinition(\"https://sbolcanvas.org/module1\")\n", - "assembly_plan = sbol2.ModuleDefinition('insert_assembly_plan')\n", - "\n", - "extracts_tuple_list, assembly_plan = part_digestion(md, [bsai], assembly_plan, pro_in_bb_doc)\n", - "part_extract, seq = extracts_tuple_list[0]\n", - "\n", - "new_doc = sbol2.Document()\n", - "for extract, sequence in extracts_tuple_list:\n", - " print(extract.identity)\n", - " print(sequence.identity)\n", - " new_doc.add(extract)\n", - " new_doc.add(sequence)\n", - "new_doc.add(assembly_plan)\n", - "new_doc.write('new_promoter_test.xml')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Backbone Digestion" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def backbone_digestion(reactant:sbol2.ModuleDefinition, restriction_enzymes:List[sbol2.ComponentDefinition], assembly_plan:sbol2.ModuleDefinition, document: sbol2.Document, **kwargs)-> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.ModuleDefinition]:\n", - " \"\"\"Digests a ModuleDefinition using the provided restriction enzymes and creates a product ComponentDefinition and a digestion Interaction.\n", - " The product ComponentDefinition is assumed to be the insert for parts in backbone and the open backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL ModuleDefinition, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used ComponentDefinition.\n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document to be used to extract referenced objects.\n", - " :return: A tuple of ComponentDefinition, Sequence, and ModuleDefinition.\n", - " \"\"\"\n", - " # extract component definition from module\n", - " reactant_displayId = reactant.functionalComponents[0].displayId\n", - " reactant_def_URI = reactant.functionalComponents[0].definition\n", - " reactant_component_definition = document.getComponentDefinition(reactant_def_URI)\n", - "\n", - " if sbol2.BIOPAX_DNA not in reactant_component_definition.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types founded {reactant.types}.')\n", - " if len(reactant_component_definition.sequences)!=1: # TODO review if true for MD, maybe for MD it will be 5\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " extracts_list=[]\n", - " restriction_enzymes_pydna=[] \n", - " \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - "\n", - " enzyme_component = sbol2.FunctionalComponent(uri=f\"{re.name}_enzyme\")\n", - " enzyme_component.definition = re\n", - " enzyme_component.displayID = f\"{re.name}_enzyme\"\n", - " enzyme_in_module = False\n", - "\n", - " for comp in assembly_plan.functionalComponents:\n", - " if (comp.displayId == enzyme_component.displayID):\n", - " enzyme_component = comp\n", - " enzyme_in_module = True\n", - "\n", - " if not enzyme_in_module:\n", - " assembly_plan.functionalComponents.add(enzyme_component)\n", - " \n", - " modifier_participation = sbol2.Participation(uri='restriction')\n", - " modifier_participation.participant = enzyme_component\n", - " modifier_participation.roles = ['http://identifiers.org/SBO:0000019']\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant_component_definition):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant_component_definition.sequences[0]\n", - " reactant_seq = document.getSequence(reactant_seq).elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna) #TODO see if ds_reactant.cut is working, causing problems downstream\n", - "\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3: \n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - "\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = backbone.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = backbone.seq.three_prime_end()\n", - " product_sequence = str(backbone.seq)\n", - " prod_backbone_definition, prod_seq = dna_componentdefinition_with_sequence2(identity=f'{reactant_component_definition.displayId}_extracted_backbone', sequence=product_sequence, **kwargs)\n", - " prod_backbone_definition.wasDerivedFrom = reactant_component_definition.identity\n", - " extracts_list.append((prod_backbone_definition, prod_seq))\n", - "\n", - " # five prime overhang\n", - " five_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_five_prime_oh\") # TODO: ensure circular type is preserved for sbh visualization\n", - " five_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001932')\n", - " five_prime_oh_location = sbol2.Range(uri=\"five_prime_oh_location\", start=1, end=len(product_5_prime_ss_end))\n", - " five_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_five_prime_oh_component\")\n", - " five_prime_oh_component.definition = five_prime_oh_definition\n", - " five_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"five_prime_overhang\")\n", - " five_prime_overhang_annotation.locations.add(five_prime_oh_location)\n", - "\n", - " # extracted backbone => point straight to backbone from sbolcanvas\n", - " backbone_location = sbol2.Range(uri=f\"{reactant_displayId}_backbone_location\", start=len(product_5_prime_ss_end)+1, end=len(product_sequence)-len(product_3_prime_ss_end))\n", - " backbone_extract_annotation = sbol2.SequenceAnnotation(uri=f\"{reactant_displayId}_backbone\")\n", - " backbone_extract_annotation.locations.add(backbone_location)\n", - " \n", - " # three prime overhang\n", - " three_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_three_prime_oh\")\n", - " three_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001933')\n", - " three_prime_oh_location = sbol2.Range(uri=\"three_prime_oh_location\", start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence)) \n", - " three_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_three_prime_oh_component\")\n", - " three_prime_oh_component.definition = three_prime_oh_definition\n", - " three_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"three_prime_overhang\")\n", - " three_prime_overhang_annotation.locations.add(three_prime_oh_location)\n", - "\n", - " anno_component_dict = {}\n", - "\n", - " prod_backbone_definition.components = [five_prime_oh_component, three_prime_oh_component]\n", - " three_prime_overhang_annotation.component = three_prime_oh_component\n", - " five_prime_overhang_annotation.component = five_prime_oh_component\n", - "\n", - " # check these lines\n", - " original_backbone_def_URI = \"\"\n", - "\n", - " #enccode ontologies of overhangs\n", - " for definition in document.componentDefinitions:\n", - " for seqURI in definition.sequences:\n", - " seq = document.getSequence(seqURI)\n", - " if seq.elements.lower() == Seq(product_3_prime_ss_end).reverse_complement():\n", - " three_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " three_prime_sequence = sbol2.Sequence(uri=f\"{three_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " three_prime_sequence.wasDerivedFrom = seq.identity\n", - " three_prime_oh_definition.sequences = [three_prime_sequence]\n", - " three_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - " \n", - " extracts_list.append((three_prime_oh_definition, three_prime_sequence))\n", - " extracts_list.append((definition, seq)) #add scars to list\n", - "\n", - " elif seq.elements.lower() == product_sequence[4:-4]: \n", - " original_backbone_def_URI = definition.identity\n", - " extracts_list.append((definition, seq))\n", - "\n", - " elif seq.elements.lower() == product_5_prime_ss_end:\n", - " five_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " five_prime_sequence = sbol2.Sequence(uri=f\"{five_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " five_prime_sequence.wasDerivedFrom = seq.identity\n", - " five_prime_oh_definition.sequences = [five_prime_sequence]\n", - " five_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - "\n", - " extracts_list.append((five_prime_oh_definition, five_prime_sequence))\n", - " extracts_list.append((definition, seq))\n", - "\n", - " #find + add original component to product def & annotation\n", - " for comp in reactant_component_definition.components: \n", - " if comp.definition == original_backbone_def_URI:\n", - " prod_backbone_definition.components.add(comp)\n", - " backbone_extract_annotation.component = comp\n", - "\n", - " prod_backbone_definition.sequenceAnnotations.add(three_prime_overhang_annotation)\n", - " prod_backbone_definition.sequenceAnnotations.add(five_prime_overhang_annotation)\n", - " prod_backbone_definition.sequenceAnnotations.add(backbone_extract_annotation)\n", - " prod_backbone_definition.addRole(tyto.SO.plasmid_vector) \n", - "\n", - " #Add reference to part in backbone\n", - " reactant_component = sbol2.FunctionalComponent(uri=f'{reactant_component_definition.displayId}_backbone_reactant')\n", - " reactant_component.definition = reactant_component_definition\n", - " assembly_plan.functionalComponents.add(reactant_component)\n", - "\n", - " # Create reactant Participation.\n", - " reactant_participation = sbol2.Participation(uri=f'{reactant_component_definition.displayId}_backbone_reactant')\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_component = sbol2.FunctionalComponent(uri=f'{reactant_component_definition.displayId}_backbone_digestion_product')\n", - " prod_component.definition = prod_backbone_definition\n", - " assembly_plan.functionalComponents.add(prod_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f'{reactant_component_definition.displayId}_backbone_product')\n", - " product_participation.participant = prod_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{reactant_component_definition.displayId}_digestion_interaction\", interaction_type='https://identifiers.org/biomodels.sbo/SBO:0000178')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - " \n", - " return extracts_list, assembly_plan" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "http://examples.org/Sequence/UJHDBOTD_extracted_backbone_seq/1\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_159_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_F/1\n", - "https://sbolcanvas.org/Scar_F_sequence\n", - "https://sbolcanvas.org/Cir_qxow/1\n", - "https://sbolcanvas.org/Cir_qxow_sequence\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_159_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n", - "should include role: https://identifiers.org/SO:0000755\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb_doc = sbol2.Document()\n", - "bb_doc.read('tests/test_files/backbone.xml')\n", - "\n", - "md = bb_doc.getModuleDefinition(\"https://sbolcanvas.org/module1\")\n", - "assembly_plan = sbol2.ModuleDefinition('bb_assembly_plan')\n", - "\n", - "extracts_tuple_list, assembly_plan = backbone_digestion(md, [bsai], assembly_plan, bb_doc)\n", - "part_extract, seq = extracts_tuple_list[0]\n", - "\n", - "new_doc = sbol2.Document()\n", - "for extract, sequence in extracts_tuple_list:\n", - " print(extract.identity)\n", - " print(sequence.identity)\n", - " new_doc.add(extract)\n", - " new_doc.add(sequence)\n", - "new_doc.add(assembly_plan)\n", - "print(f\"should include role: {tyto.SO.plasmid_vector}\")\n", - "new_doc.write('new_bb_test.xml')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ligation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target Function (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target function\n", - "def ligation(reactants:List[sbol3.Component], assembly_plan:sbol3.Component)-> List[Tuple[sbol3.Component, sbol3.Sequence]]:\n", - " \"\"\"Ligates Components using base complementarity and creates a product Component and a ligation Interaction.\n", - "\n", - " :param reactant: DNA to be ligated as SBOL Component. \n", - " :return: A tuple of Component and Interaction.\n", - " \"\"\"\n", - " # Create a dictionary that maps each first and last 4 letters to a list of strings that have those letters.\n", - " reactant_parts = []\n", - " fusion_sites_set = set()\n", - " for reactant in reactants:\n", - " fusion_site_3prime_length = reactant.features[0].locations[0].end - reactant.features[0].locations[0].start\n", - " fusion_site_5prime_length = reactant.features[0].locations[1].end - reactant.features[0].locations[1].start\n", - " if fusion_site_3prime_length == fusion_site_5prime_length:\n", - " fusion_site_length = fusion_site_3prime_length + 1 # if the fusion site is 4 bp long, the start will be 1 and end 4, 4-1 = 3, so we add 1 to get 4.\n", - " fusion_sites_set.add(fusion_site_length)\n", - " if len(fusion_sites_set) > 1:\n", - " raise ValueError(f'Fusion sites of different length within different parts. Check {reactant.identity} ')\n", - " else:\n", - " raise ValueError(f'Fusion sites of different length within the same part. Check {reactant.identity}')\n", - " if tyto.SO.plasmid_vector in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " elif tyto.SO.engineered_insert in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " else:\n", - " raise ValueError(f'Part {reactant.identity} does not have a valid role')\n", - " # remove the backbones if any from the reactants, to create the composite\n", - " groups = {}\n", - " for reactant in reactant_parts:\n", - " first_four_letters = reactant.sequences[0].lookup().elements[:fusion_site_length].lower()\n", - " last_four_letters = reactant.sequences[0].lookup().elements[-fusion_site_length:].lower()\n", - " part_syntax = f'{first_four_letters}_{last_four_letters}'\n", - " if part_syntax not in groups:\n", - " groups[part_syntax] = []\n", - " groups[part_syntax].append(reactant)\n", - " else: groups[part_syntax].append(reactant)\n", - " # groups is a dictionary of lists of parts that have the same first and last 4 letters\n", - " # list_of_combinations_per_assembly is a list of tuples of parts that can be ligated together\n", - " list_of_parts_per_combination = list(product(*groups.values())) #cartesian product\n", - " # create list_of_composites_per_assembly from list_of_combinations_per_assembly\n", - " list_of_composites_per_assembly = []\n", - " for combination in list_of_parts_per_combination:\n", - " list_of_parts_per_composite = [combination[0]] \n", - " insert_sequence = combination[0].sequences[0].lookup().elements\n", - " remaining_parts = list(combination[1:])\n", - " it = 1\n", - " while remaining_parts: \n", - " remaining_parts_before = len(remaining_parts) \n", - " for part in remaining_parts:\n", - " # match insert sequence 5' to part 3'\n", - " if part.sequences[0].lookup().elements[:fusion_site_length].lower() == insert_sequence[-fusion_site_length:].lower():\n", - " insert_sequence = insert_sequence[:-fusion_site_length] + part.sequences[0].lookup().elements\n", - " list_of_parts_per_composite.append(part)\n", - " remaining_parts.remove(part)\n", - " # match insert sequence 3' to part 5'\n", - " elif part.sequences[0].lookup().elements[-fusion_site_length:].lower() == insert_sequence[:fusion_site_length].lower():\n", - " insert_sequence = part.sequences[0].lookup().elements + insert_sequence[fusion_site_length:]\n", - " list_of_parts_per_composite.insert(0, part)\n", - " remaining_parts.remove(part)\n", - " remaining_parts_after = len(remaining_parts)\n", - " \n", - " if remaining_parts_before == remaining_parts_after:\n", - " it += 1\n", - " if it > 5: #5 was chosen arbitrarily to avoid infinite loops\n", - " print(groups)\n", - " raise ValueError('No match found, check the parts and their fusion sites')\n", - " list_of_composites_per_assembly.append(list_of_parts_per_composite)\n", - "\n", - " # transform list_of_parts_per_assembly into list of composites\n", - " products_list = []\n", - " participations = []\n", - " composite_number = 1\n", - " for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C]\n", - " # calculate sequence\n", - " composite_sequence_str = \"\"\n", - " composite_name = \"\"\n", - " #part_subcomponents = []\n", - " part_extract_subcomponents = []\n", - " for part_extract in composite:\n", - " composite_sequence_str = composite_sequence_str + part_extract.sequences[0].lookup().elements[:-fusion_site_length] #needs a version for linear\n", - " # create participations\n", - " part_extract_subcomponent = sbol3.SubComponent(part_extract) # LocalSubComponent??\n", - " part_extract_subcomponents.append(part_extract_subcomponent)\n", - " composite_name = composite_name +'_'+ part_extract.name\n", - " # create dna componente and sequence\n", - " composite_component, composite_seq = dna_component_with_sequence(f'composite_{composite_number}{composite_name}', composite_sequence_str) # **kwarads use in future?\n", - " composite_component.name = f'composite_{composite_number}{composite_name}'\n", - " composite_component.roles.append(sbol3.SO_ENGINEERED_REGION)\n", - " composite_component.features = part_extract_subcomponents\n", - " for i in range(len(composite_component.features )-1):\n", - " composite_component.constraints = [sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=composite_component.features[i], object=composite_component.features[i+1])]\n", - " products_list.append([composite_component, composite_seq])\n", - " composite_number += 1\n", - " return products_list" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Alphabetical Suffix Helper Function (for scars)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "def number_to_suffix(n):\n", - " suffix = \"\"\n", - " while n > 0:\n", - " n -= 1 # Adjust to make A = 0, B = 1, ..., Z = 25\n", - " remainder = n % 26 # Get the current character\n", - " suffix = chr(ord('A') + remainder) + suffix # Build the suffix\n", - " n = n // 26 # Move to the next higher place value\n", - " return suffix\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def ligation2(reactants:List[sbol2.ComponentDefinition], assembly_plan: sbol2.ModuleDefinition, document: sbol2.Document, ligase: sbol2.ComponentDefinition=None)->List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]:\n", - " \"\"\"Ligates Components using base complementarity and creates a product Component and a ligation Interaction.\n", - "\n", - " :param reactants: DNA parts to be ligated as SBOL ModuleDefinition. \n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document containing all reactant componentdefinitions.\n", - " :return: A tuple of ComponentDefinition and Sequence.\n", - " :param ligase: as SBOL ComponentDefinition\n", - " \"\"\"\n", - " if ligase == None:\n", - " ligase = sbol2.ComponentDefinition(uri=\"T4_Ligase\")\n", - " ligase.name = \"T4_Ligase\"\n", - " ligase.types = sbol2.BIOPAX_PROTEIN\n", - " document.add(ligase)\n", - "\n", - " ligase_component = sbol2.FunctionalComponent(uri=\"T4_Ligase\")\n", - " ligase_component.definition = ligase\n", - " assembly_plan.functionalComponents.add(ligase_component)\n", - "\n", - " modifier_participation = sbol2.Participation(uri='ligation')\n", - " modifier_participation.participant = ligase_component\n", - " modifier_participation.roles = ['http://identifiers.org/biomodels.sbo/SBO:0000019']\n", - "\n", - " # Create a dictionary that maps each first and last 4 letters to a list of strings that have those letters.\n", - " reactant_parts = []\n", - " fusion_sites_set = set()\n", - " for reactant in reactants:\n", - " fusion_site_3prime_length = reactant.sequenceAnnotations[0].locations[0].end - reactant.sequenceAnnotations[0].locations[0].start\n", - " fusion_site_5prime_length = reactant.sequenceAnnotations[1].locations[0].end - reactant.sequenceAnnotations[1].locations[0].start\n", - " if fusion_site_3prime_length == fusion_site_5prime_length:\n", - " fusion_site_length = fusion_site_3prime_length + 1 # if the fusion site is 4 bp long, the start will be 1 and end 4, 4-1 = 3, so we add 1 to get 4.\n", - " fusion_sites_set.add(fusion_site_length)\n", - " if len(fusion_sites_set) > 1:\n", - " raise ValueError(f'Fusion sites of different length within different parts. Check {reactant.identity} ')\n", - " else:\n", - " raise ValueError(f'Fusion sites of different length within the same part. Check {reactant.identity}')\n", - " if tyto.SO.plasmid_vector in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " elif tyto.SO.engineered_insert in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " else:\n", - " raise ValueError(f'Part {reactant.identity} does not have a valid role')\n", - " \n", - " # remove the backbones if any from the reactants, to create the composite\n", - " groups = {}\n", - " for reactant in reactant_parts:\n", - " reactant_seq = reactant.sequences[0]\n", - " first_four_letters = document.getSequence(reactant_seq).elements[:fusion_site_length].lower()\n", - " last_four_letters = document.getSequence(reactant_seq).elements[-fusion_site_length:].lower()\n", - " part_syntax = f'{first_four_letters}_{last_four_letters}'\n", - " if part_syntax not in groups:\n", - " groups[part_syntax] = []\n", - " groups[part_syntax].append(reactant)\n", - " else: groups[part_syntax].append(reactant)\n", - " # groups is a dictionary of lists of parts that have the same first and last 4 letters\n", - " # list_of_combinations_per_assembly is a list of tuples of parts that can be ligated together\n", - " list_of_parts_per_combination = list(product(*groups.values())) #cartesian product\n", - " # create list_of_composites_per_assembly from list_of_combinations_per_assembly\n", - " list_of_composites_per_assembly = []\n", - " for combination in list_of_parts_per_combination:\n", - " list_of_parts_per_composite = [combination[0]] \n", - " insert_sequence_uri = combination[0].sequences[0]\n", - " insert_sequence = document.getSequence(insert_sequence_uri).elements\n", - " remaining_parts = list(combination[1:])\n", - " it = 1\n", - " while remaining_parts: \n", - " remaining_parts_before = len(remaining_parts) \n", - " for part in remaining_parts:\n", - " # match insert sequence 5' to part 3'\n", - " part_sequence_uri = part.sequences[0]\n", - " if document.getSequence(part_sequence_uri).elements[:fusion_site_length].lower() == insert_sequence[-fusion_site_length:].lower():\n", - " insert_sequence = insert_sequence[:-fusion_site_length] + document.getSequence(part_sequence_uri).elements\n", - " list_of_parts_per_composite.append(part) #add sequence annotation here, index based on insert_sequence\n", - " remaining_parts.remove(part)\n", - " # match insert sequence 3' to part 5'\n", - " elif document.getSequence(part_sequence_uri).elements[-fusion_site_length:].lower() == insert_sequence[:fusion_site_length].lower():\n", - " insert_sequence = document.getSequence(part_sequence_uri).elements + insert_sequence[fusion_site_length:]\n", - " list_of_parts_per_composite.insert(0, part)\n", - " remaining_parts.remove(part)\n", - " remaining_parts_after = len(remaining_parts)\n", - " \n", - " if remaining_parts_before == remaining_parts_after:\n", - " it += 1\n", - " if it > 5: #5 was chosen arbitrarily to avoid infinite loops\n", - " print(groups)\n", - " raise ValueError('No match found, check the parts and their fusion sites')\n", - " list_of_composites_per_assembly.append(list_of_parts_per_composite)\n", - "\n", - " # transform list_of_parts_per_assembly into list of composites\n", - " products_list = []\n", - " participations = []\n", - " composite_number = 1\n", - " participations.append(modifier_participation)\n", - "\n", - " # TODO: use componentinstances to append \"subcomponents\" to each definition that is a composite component. all composites share the \"subcomponents\"\n", - " for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C]\n", - " # calculate sequence\n", - " composite_sequence_str = \"\"\n", - " composite_name = \"\"\n", - " prev_three_prime = composite[len(composite)-1].components[1].definition # componentdefinitionuri\n", - " prev_three_prime_definition = document.getComponentDefinition(prev_three_prime)\n", - " scar_index = 1\n", - " anno_list = []\n", - "\n", - " part_extract_definitions = []\n", - " for part_extract in composite:\n", - " part_extract_sequence_uri = part_extract.sequences[0]\n", - " part_extract_sequence = document.getSequence(part_extract_sequence_uri).elements\n", - " temp_extract_components = []\n", - " # TODO create participations\n", - " reactant_component = sbol2.FunctionalComponent(uri=f\"{part_extract.displayId}_reactant\")\n", - " reactant_component.definition = part_extract # TODO do not make new components, instead derive product functionalcomponents from the assembly_plan moduledefinition to add to the ligation interaction/participation\n", - " for fc in assembly_plan.functionalComponents:\n", - " if fc.definition == reactant_component.definition:\n", - " print(f\"match: {fc.displayId}, {reactant_component.displayId}\")\n", - " reactant_component = fc\n", - "\n", - " reactant_participation = sbol2.Participation(uri=f'{part_extract.displayId}_ligation')\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - "\n", - " for comp in part_extract.components:\n", - " if \"http://identifiers.org/so/SO:0001932\" in document.getComponentDefinition(comp.definition).roles: #five prime\n", - " scar_definition = sbol2.ComponentDefinition(uri=f\"Scar_{number_to_suffix(scar_index)}\")\n", - " scar_sequence = sbol2.Sequence(uri=f\"Scar_{number_to_suffix(scar_index)}_sequence\", elements=document.getSequence(prev_three_prime_definition.sequences[0]).elements)\n", - " scar_definition.sequences = [scar_sequence]\n", - " scar_definition.wasDerivedFrom = [comp.definition, prev_three_prime]\n", - " scar_definition.roles = [\"http://identifiers.org/so/SO:0001953\"]\n", - " temp_extract_components.append(scar_definition.identity)\n", - " \n", - " document.add(scar_definition)\n", - " document.add(scar_sequence)\n", - "\n", - " scar_location = sbol2.Range(uri=f\"Scar_{number_to_suffix(scar_index)}_location\", start= len(composite_sequence_str)+1, end=len(composite_sequence_str)+fusion_site_length)\n", - " scar_anno = sbol2.SequenceAnnotation(uri=f\"Scar_{number_to_suffix(scar_index)}_annotation\")\n", - " scar_anno.locations.add(scar_location)\n", - " anno_list.append(scar_anno)\n", - " scar_index += 1\n", - " elif \"http://identifiers.org/so/SO:0001933\" in document.getComponentDefinition(comp.definition).roles: #three prime\n", - " prev_three_prime = comp.definition\n", - " prev_three_prime_definition = document.getComponentDefinition(prev_three_prime)\n", - " else:\n", - " temp_extract_components.append(comp.definition)\n", - " comp_location = sbol2.Range(uri=f\"{comp.displayId}_location\", start= len(composite_sequence_str)+fusion_site_length+1, end=len(composite_sequence_str)+len(part_extract_sequence[:-4])) #TODO check if seq len is correct\n", - " comp_anno = sbol2.SequenceAnnotation(uri=f\"{comp.displayId}_annotation\")\n", - " comp_anno.locations.add(comp_location)\n", - " anno_list.append(comp_anno)\n", - "\n", - " part_extract_definitions.extend(temp_extract_components)\n", - "\n", - " composite_sequence_str = composite_sequence_str + part_extract_sequence[:-fusion_site_length] #needs a version for linear\n", - " # composite_name = composite_name + '_' + re.match(r'^[^_]*_[^_]*', part_extract.displayId).group()\n", - "\n", - " for anno in anno_list:\n", - " for location in anno.locations:\n", - " print(anno.identity, location.start, location.end)\n", - " \n", - "\n", - " # create dna component and sequence\n", - " composite_component_definition, composite_seq = dna_componentdefinition_with_sequence2(f'composite_{composite_number}', composite_sequence_str)\n", - " composite_component_definition.name = f'composite_{composite_number}'\n", - " composite_component_definition.addRole('http://identifiers.org/SO:0000804') #engineered region\n", - " composite_component_definition.addType('http://identifiers.org/so/SO:0000988')\n", - "\n", - " part_extract_components = [] \n", - " \n", - "\n", - " for i, definition in enumerate(part_extract_definitions):\n", - " def_object = document.getComponentDefinition(definition)\n", - " comp = sbol2.Component(uri=def_object.displayId)\n", - " comp.definition = definition\n", - " composite_component_definition.components.add(comp)\n", - " \n", - " anno_list[i].component = comp \n", - "\n", - " composite_component_definition.sequenceAnnotations = anno_list\n", - "\n", - " prod_functional_component = sbol2.FunctionalComponent(uri=f\"{composite_component_definition.name}\")\n", - " prod_functional_component.definition = composite_component_definition\n", - " assembly_plan.functionalComponents.add(prod_functional_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f\"{composite_component_definition.name}_product\")\n", - " product_participation.participant = prod_functional_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{composite_component_definition.name}_ligation_interaction\", interaction_type='http://identifiers.org/biomodels.sbo/SBO:0000695')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - "\n", - " products_list.append([composite_component_definition, composite_seq])\n", - " composite_number += 1\n", - " return products_list" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "reacant 1: adding part https://sbolcanvas.org/Scar_B/1\n", - "reacant 1: adding part https://sbolcanvas.org/J23101/1\n", - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "reacant 1: adding part https://sbolcanvas.org/Scar_A/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_B/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 2: adding part https://sbolcanvas.org/Scar_B/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_three_prime_oh/1\n", - "reacant 2: adding part https://sbolcanvas.org/Scar_C/1\n", - "reacant 2: adding part https://sbolcanvas.org/B0034/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "reacant 3: adding part https://sbolcanvas.org/GFP/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_C/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 3: adding part https://sbolcanvas.org/Scar_C/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_three_prime_oh/1\n", - "reacant 3: adding part https://sbolcanvas.org/Scar_D/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "reacant 4: adding part https://sbolcanvas.org/B0015/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_three_prime_oh/1\n", - "reacant 4: adding part https://sbolcanvas.org/Scar_F/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_D/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 4: adding part https://sbolcanvas.org/Scar_D/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_F/1 to Document. An object with this identity is already contained in the Document')\n", - "backbone: adding part https://sbolcanvas.org/Scar_F/1\n", - "backbone: adding part https://sbolcanvas.org/Cir_qxow/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_A/1 to Document. An object with this identity is already contained in the Document')\n", - "backbone: adding part https://sbolcanvas.org/Scar_A/1\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ligation_doc = sbol2.Document()\n", - "temp_doc = sbol2.Document()\n", - "reactants_list = []\n", - "assembly_plan = sbol2.ModuleDefinition('assembly_plan')\n", - "parts = ['tests/test_files/pro_in_bb.xml','tests/test_files/rbs_in_bb.xml', 'tests/test_files/cds_in_bb.xml', 'tests/test_files/terminator_in_bb.xml']\n", - "\n", - "for i, part in enumerate(parts):\n", - " temp_doc.read(part)\n", - " md = temp_doc.getModuleDefinition('https://sbolcanvas.org/module1')\n", - " extracts_tuple_list, assembly_plan = part_digestion(md, [bsai], assembly_plan, temp_doc)\n", - "\n", - " for extract, sequence in extracts_tuple_list:\n", - " try:\n", - " ligation_doc.add(extract)\n", - " ligation_doc.add(sequence)\n", - " except Exception as e: print(e)\n", - " print(f\"reacant {i+1}: adding part {extract}\")\n", - "\n", - " reactants_list.append(extracts_tuple_list[0][0])\n", - "\n", - "temp_doc.read('tests/test_files/backbone.xml')\n", - "# run digestion, extract component + sequence, add to ligation_doc, reactants_list\n", - "md = temp_doc.getModuleDefinition('https://sbolcanvas.org/module1')\n", - "extracts_tuple_list, assembly_plan = backbone_digestion(md, [bsai], assembly_plan, temp_doc)\n", - "for extract, seq in extracts_tuple_list:\n", - " try:\n", - " ligation_doc.add(extract)\n", - " ligation_doc.add(seq)\n", - " except Exception as e: print(e)\n", - " print(f\"backbone: adding part {extract}\")\n", - "\n", - "ligation_doc.add(assembly_plan)\n", - "reactants_list.append(extracts_tuple_list[0][0])\n", - "\n", - " \n", - "ligation_doc.add(bsai)\n", - "ligation_doc.write('ligation_test1.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_extracted_part_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_B/1\n", - "https://sbolcanvas.org/Scar_B_sequence\n", - "https://sbolcanvas.org/J23101/1\n", - "https://sbolcanvas.org/J23101_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_extracted_part_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_C/1\n", - "https://sbolcanvas.org/Scar_C_sequence\n", - "https://sbolcanvas.org/B0034/1\n", - "https://sbolcanvas.org/B0034_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_extracted_part_seq/1\n", - "https://sbolcanvas.org/GFP/1\n", - "https://sbolcanvas.org/GFP_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_D/1\n", - "https://sbolcanvas.org/Scar_D_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_extracted_part_seq/1\n", - "https://sbolcanvas.org/B0015/1\n", - "https://sbolcanvas.org/B0015_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_F/1\n", - "https://sbolcanvas.org/Scar_F_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_extracted_backbone_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_159_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Cir_qxow/1\n", - "https://sbolcanvas.org/Cir_qxow_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_159_three_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/1\n", - "http://examples.org/ComponentDefinition/BsaI/1\n", - "https://SBOL2Build.org/ComponentDefinition/T4_Ligase/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_A/1\n", - "https://SBOL2Build.org/Sequence/Scar_A_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_B/1\n", - "https://SBOL2Build.org/Sequence/Scar_B_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_C/1\n", - "https://SBOL2Build.org/Sequence/Scar_C_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_D/1\n", - "https://SBOL2Build.org/Sequence/Scar_D_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_E/1\n", - "https://SBOL2Build.org/Sequence/Scar_E_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/composite_1/1\n", - "https://SBOL2Build.org/Sequence/composite_1_seq/1\n", - "\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/T4_Ligase/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/composite_1/1\n" - ] - } - ], - "source": [ - "for obj in ligation_doc:\n", - " print(obj)\n", - "\n", - "obj = ligation_doc.find('https://SBOL2Build.org/ModuleDefinition/assembly_plan/1')\n", - "print(type(obj))\n", - "for comp in obj.functionalComponents:\n", - " print(comp.identity)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ligation Test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/three_prime_overhang/1 40 43\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/UJHDBOTD_70_part/1 5 39\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/three_prime_overhang/1 26 29\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/UJHDBOTD_45_part/1 5 25\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/three_prime_overhang/1 722 725\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/UJHDBOTD_106_part/1 5 721\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/three_prime_overhang/1 134 137\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/UJHDBOTD_134_part/1 5 133\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/three_prime_overhang/1 2200 2203\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/UJHDBOTD_159_backbone/1 5 2199\n" - ] - } - ], - "source": [ - "for definition in reactants_list:\n", - " for anno in definition.sequenceAnnotations:\n", - " for location in anno.locations:\n", - " print(anno.identity, location.start, location.end)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### WRITE TEST" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/J23101_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/B0034_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/GFP_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/B0015_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/Cir_qxow_5/1\n", - "match: UJHDBOTD_backbone_digestion_product, UJHDBOTD_extracted_backbone_reactant\n", - "match: UJHDBOTD_70_digestion_product, UJHDBOTD_70_extracted_part_reactant\n", - "match: UJHDBOTD_45_digestion_product, UJHDBOTD_45_extracted_part_reactant\n", - "match: UJHDBOTD_106_digestion_product, UJHDBOTD_106_extracted_part_reactant\n", - "match: UJHDBOTD_134_digestion_product, UJHDBOTD_134_extracted_part_reactant\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_A_annotation/1 1 4\n", - "https://SBOL2Build.org/SequenceAnnotation/Cir_qxow_5_annotation/1 5 2199\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_B_annotation/1 2200 2203\n", - "https://SBOL2Build.org/SequenceAnnotation/J23101_3_annotation/1 2204 2238\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_C_annotation/1 2239 2242\n", - "https://SBOL2Build.org/SequenceAnnotation/B0034_3_annotation/1 2243 2263\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_D_annotation/1 2264 2267\n", - "https://SBOL2Build.org/SequenceAnnotation/GFP_3_annotation/1 2268 2984\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_E_annotation/1 2985 2988\n", - "https://SBOL2Build.org/SequenceAnnotation/B0015_3_annotation/1 2989 3117\n", - "https://SBOL2Build.org/ComponentDefinition/composite_1/1\n", - "https://SBOL2Build.org/Sequence/composite_1_seq/1\n", - "https://SBOL2Build.org\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "for reactant in reactants_list:\n", - " print(f\"reactant: {reactant.components[2]}\")\n", - "pl = ligation2(reactants_list, assembly_plan, ligation_doc)\n", - "\n", - "for l in pl:\n", - " for obj in l:\n", - " print(obj.identity)\n", - " ligation_doc.add(obj)\n", - "\n", - "print(sbol2.Config.getHomespace()) \n", - "ligation_doc.write('ligation_test_forreal.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/UJHDBOTD_70_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/UJHDBOTD_70_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/UJHDBOTD_45_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/UJHDBOTD_45_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/UJHDBOTD_106_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/UJHDBOTD_106_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/UJHDBOTD_134_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/UJHDBOTD_134_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/UJHDBOTD_backbone_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/UJHDBOTD_backbone_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/T4_Ligase/1\n", - "definition = http://examples.org/ComponentDefinition/T4_Ligase/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_extracted_backbone_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_70_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_45_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_106_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_134_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/composite_1_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/composite_1/1\n", - "definition = http://examples.org/ComponentDefinition/composite_1/1\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for interaction in ligation_doc.moduleDefinitions[0].interactions:\n", - " for participation in interaction.participations:\n", - " print(participation)\n", - " print(\"participant = \", participation.participant)\n", - " print(\"definition = \", ligation_doc.find(participation.participant).definition)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ligation_doc.moduleDefinitions[0].name = \"test\"\n", - "ligation_doc.moduleDefinitions[0].name\n", - "\n", - "ligation_doc.write(\"name_test.xml\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index b9f1e14..f1e5a8f 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -12,16 +12,45 @@ "from buildcompiler.abstract_translator import extract_toplevel_definition" ] }, + { + "cell_type": "markdown", + "id": "238f2456", + "metadata": {}, + "source": [ + "## Multi-Design Lvl 1 Testing:" + ] + }, { "cell_type": "code", "execution_count": 2, "id": "e60a9c84", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['qlSBuNBL', 'i0mwvNcgH']\n" + ] + } + ], "source": [ - "design_doc = sbol2.Document()\n", - "design_doc.read(\"../tests/test_files/moclo_parts_circuit.xml\")\n", - "design = extract_toplevel_definition(design_doc)" + "design_path_list = [\n", + " \"../tests/test_files/moclo_parts_circuit.xml\",\n", + " \"../tests/test_files/mocloparts116.xml\",\n", + "]\n", + "design_defs = []\n", + "sbol_doc = sbol2.Document()\n", + "\n", + "for design in design_path_list:\n", + " temp_doc = sbol2.Document()\n", + " temp_doc.read(design)\n", + "\n", + " design_defs.append(extract_toplevel_definition(temp_doc))\n", + "\n", + " # sbol_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")\n", + "\n", + "print([design_def.displayId for design_def in design_defs])" ] }, { @@ -29,55 +58,193 @@ "execution_count": 3, "id": "90648527", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" + ] + } + ], "source": [ - "auth = \"51102d98-f852-4386-9ae8-7c5814d679c1\"\n", + "auth = \"c6f22554-f057-4b4f-a597-67041d9002e5\"\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", "]\n", - "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, None)" + "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, sbol_doc)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "7c12e504", + "execution_count": 4, + "id": "19c5d2ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://synbiohub.org/user/Gon/Enzyme_Implementations/BbsI_impl/1 https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1 https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1\n" + ] + } + ], + "source": [ + "print(buildcompiler.BbsI_impl, buildcompiler.BsaI_impl, buildcompiler.T4_ligase_impl)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6ec9e2fe", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[, ]\n", - "[]\n", - "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", - "matched pB0034_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", - "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", - "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n", - "[Plasmid:\n", - " Name: composite_1_A_B_C_D_E\n", - " Plasmid Definition: https://SBOL2Build.org/composite_1/1\n", + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23116_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n" + ] + }, + { + "data": { + "text/plain": [ + "({'https://sbolcanvas.org/qlSBuNBL/1': [Plasmid:\n", + " Name: qlSBuNBL_composite_1_A_E\n", + " Plasmid Definition: http://buildcompiler.org/qlSBuNBL_composite_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['http://buildcompiler.org/qlSBuNBL_composite_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin],\n", + " 'https://sbolcanvas.org/i0mwvNcgH/1': [Plasmid:\n", + " Name: i0mwvNcgH_composite_1_A_E\n", + " Plasmid Definition: http://buildcompiler.org/i0mwvNcgH_composite_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['http://buildcompiler.org/i0mwvNcgH_composite_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin]},\n", + " )" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product_doc = sbol2.Document()\n", + "\n", + "buildcompiler.assembly_lvl1(design_defs, product_doc)" + ] + }, + { + "cell_type": "markdown", + "id": "00834b0c", + "metadata": {}, + "source": [ + "## LVL 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e2272ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gen\n", + "Gen1\n", + "Plasmid:\n", + " Name: Gen_Gen1_plas_1_simple_A_E\n", + " Plasmid Definition: http://buildcompiler.org/Gen_Gen1_plas_1_simple/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/composite_1_impl/1']\n", + " Plasmid Implementations: ['http://buildcompiler.org/Gen_Gen1_plas_1_impl/1']\n", " Strain Implementations: [None]\n", - " Fusion Sites: ['A', 'B', 'C', 'D', 'E']\n", + " Fusion Sites: ['A', 'E']\n", " Antibiotic Resistance: Kanamycin\n", - "]\n" + "\n", + "Plasmid:\n", + " Name: Gen1_Gen1_plas_1_simple_E_F\n", + " Plasmid Definition: http://buildcompiler.org/Gen1_Gen1_plas_1_simple/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['http://buildcompiler.org/Gen1_Gen1_plas_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['E', 'F']\n", + " Antibiotic Resistance: Kanamycin\n", + "\n", + "Success with backbone: DVA_AF2_A_F and plasmids: ['Gen_Gen1_plas_1_simple_A_E', 'Gen1_Gen1_plas_1_simple_E_F']\n", + "Plasmid:\n", + " Name: DVA_AF2_A_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AF2/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVA_AF2_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + "\n", + "Comparing gctt (Gen1_Gen1_plas_1_simple_extracted_part)to gcttand reverse: cgct (Gen1_Gen1_plas_1_simple_extracted_part)to ggag\n", + "Comparing cgct (DVA_AF2_extracted_backbone)to cgctand reverse: ggag (DVA_AF2_extracted_backbone)to ggag\n" ] } ], "source": [ - "print(buildcompiler.restriction_enzyme_implementations)\n", - "print(buildcompiler.ligase_implementations)\n", + "lvl2_design_doc = sbol2.Document()\n", + "lvl2_design_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")\n", "\n", - "composite_plasmids = buildcompiler.assembly_lvl1(design, None)" + "\n", + "composite_plasmids, final_doc = buildcompiler.assembly_lvl2(\n", + " lvl2_design_doc, product_name=\"lvl2\"\n", + ")" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, + "id": "ba31c4c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Plasmid:\n", + " Name: lvl2_1_A_F\n", + " Plasmid Definition: http://buildcompiler.org/lvl2_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['http://buildcompiler.org/lvl2_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + "]\n" + ] + }, + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(composite_plasmids)\n", + "final_doc.write(\"lvl2_assembly.xml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, "id": "79fd0cb5", "metadata": {}, "outputs": [], @@ -109,22 +276,10 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "8f4ea67c", "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'composite_plasmids' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m bacterial_transformation(\u001b[43mcomposite_plasmids\u001b[49m, chassis_implementation, chassis, final_doc)\n", - "\u001b[31mNameError\u001b[39m: name 'composite_plasmids' is not defined" - ] - } - ], + "outputs": [], "source": [ "# bacterial_transformation(composite_plasmids, chassis_implementation, chassis, final_doc)" ] @@ -132,23 +287,10 @@ { "cell_type": "code", "execution_count": null, - "id": "5553bfc3", + "id": "cd334add", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# final_doc.write(\"fullbuild.xml\")" - ] + "outputs": [], + "source": [] } ], "metadata": { diff --git a/notebooks/comb_design.ipynb b/notebooks/comb_design.ipynb index ead09ff..dcc14c7 100644 --- a/notebooks/comb_design.ipynb +++ b/notebooks/comb_design.ipynb @@ -2,19 +2,18 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "id": "814099af", "metadata": {}, "outputs": [], "source": [ "import sbol2\n", - "from typing import Dict, List\n", - "from sbol2build.abstract_translator import extract_design_parts" + "from buildcompiler.buildcompiler import BuildCompiler" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 6, "id": "2f1ab216", "metadata": {}, "outputs": [ @@ -22,386 +21,330 @@ "name": "stdout", "output_type": "stream", "text": [ - "Design........................0\n", - "Build.........................0\n", - "Test..........................0\n", - "Analysis......................0\n", - "ComponentDefinition...........5\n", - "ModuleDefinition..............0\n", - "Model.........................0\n", - "Sequence......................4\n", - "Collection....................0\n", - "Activity......................0\n", - "Plan..........................0\n", - "Agent.........................0\n", - "Attachment....................0\n", - "CombinatorialDerivation.......1\n", - "Implementation................0\n", - "SampleRoster..................0\n", - "Experiment....................0\n", - "ExperimentalData..............0\n", - "Annotation Objects............5\n", - "---\n", - "Total: .........................15\n", - "\n" + "https://sbolcanvas.org/combinatorialRBSs\n" ] } ], "source": [ "abstract_doc = sbol2.Document()\n", "abstract_doc.read(\"tests/test_files/combinatorial_1.xml\")\n", - "print(abstract_doc)" + "comb_design = abstract_doc.combinatorialderivations[0]\n", + "print(comb_design.identity)" ] }, { "cell_type": "code", "execution_count": 3, - "id": "f0fb25d5", + "id": "39f37870", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://sbolcanvas.org/abstract_combinatorial/1\n" + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" ] } ], "source": [ - "toplevel = abstract_doc.componentDefinitions[\n", - " 2\n", - "] # TODO develop approach to extract toplevel definition in non MD documents; maybe check for annotations or subcomponents\n", - "print(toplevel)" + "auth = \"47c58a25-78d5-4aee-b74b-9cd2e25da55e\"\n", + "collections = [\n", + " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", + " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", + "]\n", + "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, abstract_doc)" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "c36c9c45", + "execution_count": null, + "id": "c3bbf89a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0033_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0040_CD_C_D with pB0033_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0033_BC_B_C', 'pE0040_CD_C_D', 'pB0015_DE_D_E']\n", + "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0032_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0040_CD_C_D with pB0032_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" + ] + } + ], "source": [ - "parts = extract_design_parts(toplevel, abstract_doc)" + "list = buildcompiler.assembly_lvl1(comb_design)" ] }, { "cell_type": "code", "execution_count": 5, - "id": "bf3932d9", + "id": "3d70b565", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1\n", - "https://sbolcanvas.org/RBS_HFQr/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n" + "({'https://sbolcanvas.org/abstract_combinatorial/1': [Plasmid:\n", + " Name: abstract_combinatorial_composite_comb0_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb1_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "]}, )\n" ] } ], "source": [ - "for part in parts:\n", - " print(part)" + "print(list)" ] }, { "cell_type": "code", "execution_count": 6, - "id": "ecd909f4", - "metadata": {}, - "outputs": [], - "source": [ - "def extract_combinatorial_design_parts(\n", - " design: sbol2.ComponentDefinition, doc: sbol2.Document, plasmid_doc\n", - ") -> Dict[str, List[sbol2.ComponentDefinition]]:\n", - " \"\"\"\n", - " Extracts and returns a mapping of component definitions from a combinatorial design, in order.\n", - "\n", - " Retrieves the components in sequential order from the given design.\n", - " Variants of combinatinatorial components are entered in a list corresponding to the URI of the component in the abstract design.\n", - "\n", - " Args:\n", - " design:\n", - " The :class:`sbol2.ComponentDefinition` representing the top-level design\n", - " from which to extract parts.\n", - " doc:\n", - " The primary :class:`sbol2.Document` containing the base component definitions\n", - " and combinatorial derivations.\n", - " plasmid_doc:\n", - " An additional :class:`sbol2.Document` used to resolve component variants\n", - " (plasmid-specific variants referenced by combinatorial derivations).\n", - "\n", - " Returns:\n", - " Dict[str, List[sbol2.ComponentDefinition]]:\n", - " A dictionary mapping component identities to lists\n", - " of corresponding component definitions.\n", - "\n", - " - Sequential design components map to lists containing a single definition.\n", - " - Combinatorial variable components map to lists of variant definitions.\n", - " \"\"\"\n", - " component_list = [c for c in design.getInSequentialOrder()]\n", - " component_dict = {\n", - " component.identity: [doc.getComponentDefinition(component.definition)]\n", - " for component in component_list\n", - " }\n", - "\n", - " for deriv in doc.combinatorialderivations:\n", - " for component in deriv.variableComponents:\n", - " component_dict[component.variable] = [\n", - " plasmid_doc.getComponentDefinition(var) for var in component.variants\n", - " ]\n", - "\n", - " return component_dict" - ] - }, - { - "cell_type": "markdown", - "id": "0ee4be8f", - "metadata": {}, - "source": [ - "### Problem: variant definitions are not included in sbolCanvas abstract combinatorial design document, only references\n", - "#### Solution: references can be resolved by pulling from plasmid collection" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "4fef4173", - "metadata": {}, - "outputs": [], - "source": [ - "plasmid_collection = sbol2.Document()\n", - "sbh = sbol2.PartShop(\"https://synbiohub.org\")\n", - "sbh.pull(\n", - " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1/b7fdc21c6601a61d3166073a9e50f2c3843e1df5/share\",\n", - " plasmid_collection,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "5d87a7fd", + "id": "99b474f2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://sbolcanvas.org/abstract_combinatorial/J23100_1/1:\n", - "J23100\n", - "https://sbolcanvas.org/abstract_combinatorial/RBS_HFQr_2/1:\n", - "B0032\n", - "B0033\n", - "https://sbolcanvas.org/abstract_combinatorial/E0040m_gfp_3/1:\n", - "E0040m_gfp\n", - "https://sbolcanvas.org/abstract_combinatorial/B0015_4/1:\n", - "B0015\n" + "[Plasmid:\n", + " Name: pE0040_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0040_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0033_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0032_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DG_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DH_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE1010_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'E']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb0_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb1_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "]\n" ] } ], "source": [ - "dict = extract_combinatorial_design_parts(toplevel, abstract_doc, plasmid_collection)\n", - "\n", - "for key, value in dict.items():\n", - " print(f\"{key}:\")\n", - " for element in value:\n", - " print(element.displayId)" + "print(buildcompiler.indexed_plasmids)" ] }, { - "cell_type": "code", - "execution_count": 9, - "id": "59be7c31", - "metadata": {}, - "outputs": [], - "source": [ - "import itertools\n", - "\n", - "\n", - "def enumerate_design_variants(component_dict):\n", - " \"\"\"\n", - " Given a dict mapping variable component identities to lists of ComponentDefinitions,\n", - " generate all possible design combinations as lists of ComponentDefinitions\n", - " (in consistent order of keys).\n", - " \"\"\"\n", - " keys = list(component_dict.keys())\n", - " variant_lists = [component_dict[k] for k in keys]\n", - "\n", - " # Cartesian product across all variant lists\n", - " all_variants = list(itertools.product(*variant_lists))\n", - "\n", - " # Convert tuples to lists (optional) for clarity\n", - " all_variants = [list(combo) for combo in all_variants]\n", - "\n", - " return all_variants" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f2c774c9", - "metadata": {}, - "outputs": [], - "source": [ - "from sbol2build.abstract_translator import (\n", - " construct_plasmid_dict,\n", - " extract_toplevel_definition,\n", - " get_compatible_plasmids,\n", - " MocloPlasmid,\n", - ")\n", - "\n", - "enumerated = enumerate_design_variants(dict)\n", - "\n", - "plasmid_dicts = []\n", - "\n", - "# for design in enumerated:\n", - "# plasmid_dicts.append(construct_plasmid_dict(design, plasmid_collection))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "0470f3f9", + "cell_type": "markdown", + "id": "0ee4be8f", "metadata": {}, - "outputs": [], "source": [ - "def translate_abstract_to_plasmids(\n", - " abstract_design_doc: sbol2.Document,\n", - " plasmid_collection: sbol2.Document,\n", - " backbone_doc: sbol2.Document,\n", - "):\n", - " backbone_def = extract_toplevel_definition(backbone_doc)\n", - " backbone_plasmid = MocloPlasmid(backbone_def.displayId, backbone_def, backbone_doc)\n", - "\n", - " # combinatorial design\n", - " if len(abstract_design_doc.combinatorialderivations) > 0:\n", - " abstract_design_def = abstract_design_doc.getComponentDefinition(\n", - " abstract_design_doc.combinatorialderivations[0].masterTemplate\n", - " )\n", - "\n", - " combinatorial_part_dict = extract_combinatorial_design_parts(\n", - " abstract_design_def, abstract_design_doc, plasmid_collection\n", - " )\n", - " enumerated_part_list = enumerate_design_variants(combinatorial_part_dict)\n", - "\n", - " final_plasmid_list = []\n", - "\n", - " for design in enumerated_part_list:\n", - " plasmid_dict = construct_plasmid_dict(design, plasmid_collection)\n", - " final_plasmid_list += get_compatible_plasmids(\n", - " plasmid_dict, backbone_plasmid\n", - " )\n", - "\n", - " return set(final_plasmid_list)\n", - "\n", - " # generic design\n", - " else:\n", - " abstract_design_def = extract_toplevel_definition(abstract_design_doc)\n", - "\n", - " ordered_part_definitions = extract_design_parts(\n", - " abstract_design_def, abstract_design_doc\n", - " )\n", - "\n", - " plasmid_dict = construct_plasmid_dict(\n", - " ordered_part_definitions, plasmid_collection\n", - " )\n", - "\n", - " return get_compatible_plasmids(plasmid_dict, backbone_plasmid)" + "### Problem: variant definitions are not included in sbolCanvas abstract combinatorial design document, only references\n", + "#### Solution: references can be resolved by pulling from plasmid collection" ] }, { - "cell_type": "code", - "execution_count": 19, - "id": "97ee6712", + "cell_type": "markdown", + "id": "b592c69c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1 with ['Fusion_Site_B', 'Fusion_Site_A']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1 with ['Fusion_Site_B', 'Fusion_Site_G']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1 with ['Fusion_Site_F', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1 with ['Fusion_Site_B', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0032/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1 with ['Fusion_Site_C', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1 with ['Fusion_Site_D', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1 with ['Fusion_Site_G', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1 with ['Fusion_Site_F', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1 with ['Fusion_Site_D', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1 with ['Fusion_Site_D', 'Fusion_Site_H']\n", - "matched J23100_A_B with DVK_AE_A_E on fusion site A!\n", - "matched B0032_B_C with J23100_A_B on fusion site B!\n", - "matched E0040m_gfp_C_D with B0032_B_C on fusion site C!\n", - "matched final component B0015_D_E with E0040m_gfp_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1 with ['Fusion_Site_B', 'Fusion_Site_A']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1 with ['Fusion_Site_B', 'Fusion_Site_G']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1 with ['Fusion_Site_F', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1 with ['Fusion_Site_B', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0033/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1 with ['Fusion_Site_B', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1 with ['Fusion_Site_D', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1 with ['Fusion_Site_G', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1 with ['Fusion_Site_F', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1 with ['Fusion_Site_D', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1 with ['Fusion_Site_D', 'Fusion_Site_H']\n", - "matched J23100_A_B with DVK_AE_A_E on fusion site A!\n", - "matched B0033_B_C with J23100_A_B on fusion site B!\n", - "matched E0040m_gfp_C_D with B0033_B_C on fusion site C!\n", - "matched final component B0015_D_E with E0040m_gfp_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" - ] - }, - { - "data": { - "text/plain": [ - "{MocloPlasmid:\n", - " Name: B0015_D_E\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", - " Fusion Sites: ['D', 'E'],\n", - " MocloPlasmid:\n", - " Name: B0032_B_C\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", - " Fusion Sites: ['B', 'C'],\n", - " MocloPlasmid:\n", - " Name: B0033_B_C\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", - " Fusion Sites: ['B', 'C'],\n", - " MocloPlasmid:\n", - " Name: E0040m_gfp_C_D\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", - " Fusion Sites: ['C', 'D'],\n", - " MocloPlasmid:\n", - " Name: J23100_A_B\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", - " Fusion Sites: ['A', 'B']}" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb_doc = sbol2.Document()\n", - "sbh.pull(\n", - " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1/647c5b2458567dcce6b0a37178d352b8ffa9a7fe/share\",\n", - " bb_doc,\n", - ")\n", - "\n", - "translate_abstract_to_plasmids(\n", - " abstract_design_doc=abstract_doc,\n", - " plasmid_collection=plasmid_collection,\n", - " backbone_doc=bb_doc,\n", - ")" - ] + "source": [] }, { - "cell_type": "code", - "execution_count": null, - "id": "06f2668c", + "cell_type": "markdown", + "id": "42a819eb", "metadata": {}, - "outputs": [], "source": [] } ], diff --git a/notebooks/enzyme_creation.ipynb b/notebooks/enzyme_creation.ipynb new file mode 100644 index 0000000..d26eec2 --- /dev/null +++ b/notebooks/enzyme_creation.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "87bdb42e", + "metadata": {}, + "outputs": [], + "source": [ + "import sbol2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "00a6e6c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from buildcompiler.sbol2build import rebase_restriction_enzyme, add_object_to_doc\n", + "\n", + "sbol2.Config.setHomespace(\"https://SBOL2Build.org\")\n", + "sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True)\n", + "sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False)\n", + "\n", + "protein_doc = sbol2.Document()\n", + "\n", + "\n", + "BsaI_def = rebase_restriction_enzyme(\"BsaI\")\n", + "\n", + "RE_sourcing = sbol2.Activity(\"restriction_enzyme_purchase\")\n", + "RE_sourcing.name = \"Restriction Enzyme Purchase\"\n", + "\n", + "BsaI_imp = sbol2.Implementation(f\"{BsaI_def.displayId}_impl\")\n", + "\n", + "BsaI_imp.built = BsaI_def.identity\n", + "BsaI_imp.wasGeneratedBy = RE_sourcing.identity\n", + "\n", + "BbsI_def = rebase_restriction_enzyme(\"BbsI\")\n", + "\n", + "BbsI_imp = sbol2.Implementation(f\"{BbsI_def.displayId}_impl\")\n", + "\n", + "BbsI_imp.built = BbsI_def.identity\n", + "BbsI_imp.wasGeneratedBy = RE_sourcing.identity\n", + "\n", + "\n", + "ligase = sbol2.ComponentDefinition(\"T4_Ligase\")\n", + "ligase.name = \"T4_Ligase\"\n", + "ligase.types = [sbol2.BIOPAX_PROTEIN]\n", + "ligase.roles = [\"http://identifiers.org/ncit/NCIT:C16796\"]\n", + "\n", + "ligase_sourcing = sbol2.Activity(\"ligase_purchase\")\n", + "ligase_sourcing.name = \"Ligase Purchase\"\n", + "\n", + "T4_imp = sbol2.Implementation(f\"{ligase.displayId}_impl\")\n", + "T4_imp.built = ligase.identity\n", + "T4_imp.wasGeneratedBy = ligase_sourcing.identity\n", + "\n", + "add_object_to_doc(ligase, protein_doc)\n", + "add_object_to_doc(T4_imp, protein_doc)\n", + "add_object_to_doc(BsaI_def, protein_doc)\n", + "add_object_to_doc(BsaI_imp, protein_doc)\n", + "add_object_to_doc(BbsI_def, protein_doc)\n", + "add_object_to_doc(BbsI_imp, protein_doc)\n", + "add_object_to_doc(RE_sourcing, protein_doc)\n", + "\n", + "protein_doc.write(\"proteins.xml\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f7433c3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (buildplanner)", + "language": "python", + "name": "buildplanner" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/full_build_quickstart.ipynb b/notebooks/full_build_quickstart.ipynb new file mode 100644 index 0000000..efbc33b --- /dev/null +++ b/notebooks/full_build_quickstart.ipynb @@ -0,0 +1,193 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BuildCompiler `full_build(...)` Quickstart\n", + "\n", + "This notebook shows how to run the high-level `BuildCompiler.full_build(...)` workflow end-to-end.\n", + "\n", + "The orchestrated stages are:\n", + "1. domestication (only when missing parts are detected)\n", + "2. assembly level 1\n", + "3. transformation\n", + "4. plating\n", + "\n", + "`assembly_lvl2` is intentionally skipped for now and recorded as skipped in the output manifest." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1) Imports and setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sbol2\n", + "\n", + "from buildcompiler.buildcompiler import BuildCompiler" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2) Load an SBOL design document\n", + "\n", + "Replace the path below with your own SBOL file containing one or more abstract designs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "doc = sbol2.Document()\n", + "# Example: doc.read(\"path/to/your/designs.xml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3) Construct the compiler\n", + "\n", + "Provide one or more SynBioHub collections that contain available parts/backbones/implementations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "compiler = BuildCompiler(\n", + " collections=[\n", + " # \"https://synbiohub.org/public/example_collection\",\n", + " ],\n", + " sbh_registry=\"https://synbiohub.org\",\n", + " auth_token=\"\",\n", + " sbol_doc=doc,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4) Full build with concrete designs (list of `ComponentDefinition`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example lookup by displayId (adjust to your document)\n", + "# design_1 = doc.getComponentDefinition(\"design_1\")\n", + "# design_2 = doc.getComponentDefinition(\"design_2\")\n", + "# designs = [design_1, design_2]\n", + "\n", + "# result = compiler.full_build(\n", + "# designs=designs,\n", + "# results_dir=Path(\"results/full_build\"),\n", + "# chassis_name=\"E_coli_DH5alpha\",\n", + "# protocol_type=\"manual\",\n", + "# plating_params={\n", + "# \"incubation_temperature_c\": 37,\n", + "# \"incubation_time_h\": 16,\n", + "# },\n", + "# product_name_prefix=\"build\",\n", + "# overwrite=True,\n", + "# )\n", + "\n", + "# print(result[\"status\"])\n", + "# print(result[\"manifest_path\"])\n", + "# print(result[\"zip_path\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5) Full build with a `CombinatorialDerivation`\n", + "\n", + "Use this when your input is combinatorial. `full_build(...)` expands the derivation into concrete variants automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# derivation = doc.combinatorialderivations[0]\n", + "\n", + "# combo_result = compiler.full_build(\n", + "# designs=derivation,\n", + "# results_dir=Path(\"results/full_build_combinatorial\"),\n", + "# chassis_name=\"E_coli_DH5alpha\",\n", + "# protocol_type=\"automated\",\n", + "# plating_params={\n", + "# \"replicates\": 1,\n", + "# \"number_dilutions\": 1,\n", + "# \"volume_colony\": 6,\n", + "# \"thermocycler_starting_well\": 0,\n", + "# },\n", + "# product_name_prefix=\"combo_build\",\n", + "# overwrite=True,\n", + "# )\n", + "\n", + "# print(combo_result[\"status\"])\n", + "# print(combo_result[\"manifest_path\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6) Return shape and generated files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Top-level keys in returned result:\n", + "# stage, status, results_dir, zip_path, manifest_path, sbol_path,\n", + "# inputs, domestication, assembly_lvl1, transformation, plating, skipped, errors\n", + "\n", + "# Files written under :\n", + "# - full_build_manifest.json\n", + "# - full_build_results.zip\n", + "# - sbol/full_build.xml\n", + "# - domestication/* and assembly_lvl1/* plating/transformation artifacts" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/impl_creation.ipynb b/notebooks/impl_creation.ipynb new file mode 100644 index 0000000..6105579 --- /dev/null +++ b/notebooks/impl_creation.ipynb @@ -0,0 +1,516 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "87bdb42e", + "metadata": {}, + "outputs": [], + "source": [ + "import sbol2\n", + "from buildcompiler.buildcompiler import BuildCompiler" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "90648527", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Indexing collection: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\n" + ] + } + ], + "source": [ + "auth = \"839935f0-7f6a-4c83-b1f0-91ddc0eb9c9a\"\n", + "buildcompiler = BuildCompiler(\n", + " [\n", + " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\"\n", + " ],\n", + " \"https://synbiohub.org\",\n", + " auth,\n", + " sbol2.Document(),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "99d093a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Plasmid:\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'E']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0040_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + "]\n" + ] + } + ], + "source": [ + "print(buildcompiler.indexed_plasmids)" + ] + }, + { + "cell_type": "markdown", + "id": "eb4a9ab0", + "metadata": {}, + "source": [ + "# Create Collection of Implementations To Test" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "9cdd18d0", + "metadata": {}, + "outputs": [], + "source": [ + "implementation_collection = sbol2.Document()\n", + "plas_doc = sbol2.Document()\n", + "\n", + "buildcompiler.sbh.pull(\n", + " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\",\n", + " plas_doc,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "d76dbdeb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "amp_region\n", + "B0033\n", + "dvk_backbone_core\n", + "J23106\n", + "E0030_yfp\n", + "kan_region\n", + "E0040m_gfp\n", + "B0015\n", + "J23116\n", + "J23100\n", + "Fusion_Site_E\n", + "Fusion_Site_F\n", + "Fusion_Site_D\n", + "Fusion_Site_G\n", + "E1010m_rfp\n", + "Fusion_Site_A\n", + "LacZ_cassette\n", + "Fusion_Site_B\n", + "Fusion_Site_C\n", + "LacZ_cassette2\n", + "Fusion_Site_H\n", + "B0032\n", + "B0034\n", + "dva_backbone_core\n", + "origin_of_replication_pSB1A2\n", + "Design........................0\n", + "Build.........................0\n", + "Test..........................0\n", + "Analysis......................0\n", + "ComponentDefinition...........0\n", + "ModuleDefinition..............0\n", + "Model.........................0\n", + "Sequence......................0\n", + "Collection....................0\n", + "Activity......................0\n", + "Plan..........................0\n", + "Agent.........................0\n", + "Attachment....................0\n", + "CombinatorialDerivation.......0\n", + "Implementation................36\n", + "SampleRoster..................0\n", + "Experiment....................0\n", + "ExperimentalData..............0\n", + "Annotation Objects............0\n", + "---\n", + "Total: .........................36\n", + "\n" + ] + } + ], + "source": [ + "from buildcompiler.constants import ENGINEERED_PLASMID, CIRCULAR, PLASMID_CLONING_VECTOR\n", + "\n", + "\n", + "implementation_collection.default_namespace = (\n", + " \"http://buildcompiler.org/implementations/\"\n", + ")\n", + "\n", + "dummy_activity = sbol2.Activity(\"plasmid_dna_extraction\")\n", + "dummy_activity.name = \"DNA extraction\"\n", + "dummy_activity.types = \"http://sbols.org/v2#build\"\n", + "\n", + "for plasmid in plas_doc.componentDefinitions:\n", + " if (\n", + " ENGINEERED_PLASMID or PLASMID_CLONING_VECTOR in plasmid.roles\n", + " ) and CIRCULAR in plasmid.types:\n", + " implementation = sbol2.Implementation(f\"{plasmid.displayId}_impl\")\n", + " implementation.built = plasmid.identity\n", + " implementation.wasGeneratedBy = dummy_activity\n", + "\n", + " implementation_collection.add(implementation)\n", + " else:\n", + " print(plasmid.displayId)\n", + "\n", + "print(implementation_collection)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "3bcf02fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "implementation_collection.write(\"moclo_implementations.xml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "9bf9c396", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['pB0034_BC_impl',\n", + " 'pJ23100_GB_impl',\n", + " 'DVA_DF_impl',\n", + " 'DVA_DG_impl',\n", + " 'DVA_AF_impl',\n", + " 'pB0015_DH_impl',\n", + " 'pB0032_BC_impl',\n", + " 'pE1010_CD_impl',\n", + " 'DVA_CD_impl',\n", + " 'DVA_GB_impl',\n", + " 'DVA_AF2_impl',\n", + " 'pJ23116_FB_impl',\n", + " 'pB0015_DE_impl',\n", + " 'pJ23100_EB_impl',\n", + " 'pE0040_CD_impl',\n", + " 'DVK_GH_impl',\n", + " 'DVA_DH_impl',\n", + " 'pJ23116_EB_impl',\n", + " 'pJ23106_EB_impl',\n", + " 'pJ23116_GB_impl',\n", + " 'DVK_AE_impl',\n", + " 'pJ23116_AB_impl',\n", + " 'DVA_BC_impl',\n", + " 'pB0033_BC_impl',\n", + " 'pJ23106_AB_impl',\n", + " 'pB0015_DG_impl',\n", + " 'pE0030_CD_impl',\n", + " 'DVK_EF_impl',\n", + " 'DVA_FB_impl',\n", + " 'pJ23106_GB_impl',\n", + " 'DVA_AB_impl',\n", + " 'DVK_FG_impl',\n", + " 'pJ23100_AB_impl',\n", + " 'pJ23100_FB_impl',\n", + " 'pB0015_DF_impl',\n", + " 'pJ23106_FB_impl']" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[cd.displayId for cd in implementation_collection.implementations]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7b8369e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (buildplanner)", + "language": "python", + "name": "buildplanner" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index b391319..27cf445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,11 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "sbol2build" -version = "0.0.b3" -description = "SBOL2Build: python package for construction of SBOL objects during build planning" +name = "synbio-buildcompiler" +version = "0.0.a1" +description = "BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" license = {file = "LICENSE.md"} keywords = ["SBOL", "genetic", "automation", "build", "synthetic biology"] authors = [ @@ -31,15 +31,26 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest < 5.0.0", + "pytest>=7,<9", "pytest-cov[all]" ] +automation = [ + "pudupy", + "opentrons", + "SBOLInventory @ git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; python_version >= '3.10'", +] [project.urls] -"Homepage" = "https://github.com/MyersResearchGroup/SBOL2Build" -"Bug Tracker" = "https://github.com/MyersResearchGroup/SBOL2Build/issues" +"Homepage" = "https://github.com/MyersResearchGroup/BuildCompiler" +"Bug Tracker" = "https://github.com/MyersResearchGroup/BuildCompiler/issues" [dependency-groups] dev = [ "ruff>=0.14.0", ] + + +[tool.pytest.ini_options] +markers = [ + "automation: optional/manual automation tests", +] diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index 5c2ef02..327c248 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -90,7 +90,7 @@ def extract_fusion_sites( A list of fusion site component definitions. """ fusion_sites = [] - for component in plasmid.components: + for component in plasmid.getInSequentialOrder(): definition = doc.getComponentDefinition(component.definition) if RESTRICTION_ENZYME_ASSEMBLY_SCAR in definition.roles: fusion_sites.append(definition) @@ -135,14 +135,31 @@ def copy_sequences(component_definition, target_doc, collection_doc): seq_obj.copy(target_doc) -def get_or_pull(doc, sbh, uri): +def get_or_pull( + doc: sbol2.Document, sbh: sbol2.PartShop, uri: str, server_mode: bool = False +): """ Get an SBOL object from a Document. If missing, pull it from SynBioHub and retry. """ - if uri not in doc: - sbh.pull(uri, doc) - return doc.get(uri) + + try: + return doc.get(uri) + + except Exception as e: + pull_uri = uri + + if server_mode: + canonical_resource = sbh.resource.replace("://api.", "://") + + pull_uri = uri.replace(canonical_resource, sbh.resource) + + sbh.pull(pull_uri, doc) + + try: + return doc.get(uri) + except Exception: + raise e def extract_combinatorial_design_parts( @@ -187,7 +204,30 @@ def extract_combinatorial_design_parts( def extract_toplevel_definition(doc: sbol2.Document) -> sbol2.ComponentDefinition: - return doc.componentDefinitions[0] + cds = list(doc.componentDefinitions) + + # identities of definitions used as subcomponents + used_defs = set() + + for cd in cds: + for comp in cd.components: + used_defs.add(comp.definition) + + # candidates = composite designs not used inside another design + candidates = [ + cd for cd in cds if len(cd.components) > 0 and cd.identity not in used_defs + ] + + if len(candidates) == 1: + return candidates[0] + + if len(candidates) == 0: + raise ValueError("No top-level composite ComponentDefinition found") + + raise ValueError( + f"Multiple top-level ComponentDefinitions found: " + f"{[c.displayId for c in candidates]}" + ) def enumerate_design_variants(component_dict): @@ -290,9 +330,6 @@ def get_compatible_plasmids( and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx] and plasmid.fusion_sites[1] == backbone.fusion_sites[1] ): - print( - f"matched final component {plasmid.name} with {match_to.name} and {backbone.name} on fusion sites ({plasmid.fusion_sites[0]}, {plasmid.fusion_sites[1]})!" - ) selected_plasmids.append(plasmid) found = True break @@ -300,9 +337,6 @@ def get_compatible_plasmids( i < len(plasmid_dict) - 1 and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx] ): - print( - f"matched {plasmid.name} with {match_to.name} on fusion site {plasmid.fusion_sites[0]}!" - ) selected_plasmids.append(plasmid) found = True match_to = plasmid diff --git a/src/buildcompiler/adapters/__init__.py b/src/buildcompiler/adapters/__init__.py new file mode 100644 index 0000000..3390756 --- /dev/null +++ b/src/buildcompiler/adapters/__init__.py @@ -0,0 +1,5 @@ +"""Adapter package exports without optional dependency side effects.""" + +from .protocols import ProtocolArtifact, maybe_write_protocol_artifacts + +__all__ = ["ProtocolArtifact", "maybe_write_protocol_artifacts"] diff --git a/src/buildcompiler/adapters/opentrons/__init__.py b/src/buildcompiler/adapters/opentrons/__init__.py new file mode 100644 index 0000000..48db553 --- /dev/null +++ b/src/buildcompiler/adapters/opentrons/__init__.py @@ -0,0 +1,13 @@ +"""Optional Opentrons adapter exports.""" + +from .simulation import ( + OpentronsSimulationAdapter, + OptionalAutomationDependencyError, + SimulationResult, +) + +__all__ = [ + "OpentronsSimulationAdapter", + "OptionalAutomationDependencyError", + "SimulationResult", +] diff --git a/src/buildcompiler/adapters/opentrons/simulation.py b/src/buildcompiler/adapters/opentrons/simulation.py new file mode 100644 index 0000000..a8c8e17 --- /dev/null +++ b/src/buildcompiler/adapters/opentrons/simulation.py @@ -0,0 +1,44 @@ +"""Optional Opentrons simulation boundary adapter.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from buildcompiler.api import ProtocolOptions + + +class OptionalAutomationDependencyError(ImportError): + """Raised when optional automation dependency is unavailable.""" + + +@dataclass +class SimulationResult: + ran: bool + logs: list[str] = field(default_factory=list) + metadata: dict[str, object] = field(default_factory=dict) + + +class OpentronsSimulationAdapter: + def simulate( + self, protocol_source: str | Path, *, options: ProtocolOptions + ) -> SimulationResult: + if not options.simulate: + return SimulationResult( + ran=False, + logs=["Simulation skipped: ProtocolOptions.simulate is False."], + metadata={"protocol_source": str(protocol_source)}, + ) + + try: + __import__("opentrons") + except ImportError as exc: + raise OptionalAutomationDependencyError( + "Install synbio-buildcompiler[automation] to use Opentrons simulation." + ) from exc + + return SimulationResult( + ran=True, + logs=["Simulation dependency check passed."], + metadata={"protocol_source": str(protocol_source)}, + ) diff --git a/src/buildcompiler/adapters/protocols.py b/src/buildcompiler/adapters/protocols.py new file mode 100644 index 0000000..92c8e7b --- /dev/null +++ b/src/buildcompiler/adapters/protocols.py @@ -0,0 +1,55 @@ +"""Protocol artifact boundaries for optional file output.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path + +from buildcompiler.api import ProtocolMode, ProtocolOptions + + +@dataclass +class ProtocolArtifact: + kind: str + path: Path | None = None + content: str | dict[str, object] | list[dict[str, object]] | None = None + metadata: dict[str, object] = field(default_factory=dict) + + +def _artifact_filename(*, basename: str, kind: str) -> str: + safe_kind = kind.replace(" ", "_").lower() + return f"{basename}_{safe_kind}.json" + + +def maybe_write_protocol_artifacts( + *, + payloads: dict[str, object], + options: ProtocolOptions, + basename: str = "buildcompiler_protocol", +) -> dict[str, ProtocolArtifact]: + """Return in-memory protocol payloads and optionally write them to disk.""" + + should_write = ( + options.mode in {ProtocolMode.MANUAL, ProtocolMode.AUTOMATED} + and options.results_dir is not None + ) + output_dir = Path(options.results_dir) if should_write else None + if output_dir is not None: + output_dir.mkdir(parents=True, exist_ok=True) + + artifacts: dict[str, ProtocolArtifact] = {} + for kind, payload in payloads.items(): + artifact = ProtocolArtifact(kind=kind, content=payload) + if output_dir is not None: + path = output_dir / _artifact_filename(basename=basename, kind=kind) + path.write_text( + json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8" + ) + artifact.path = path + artifact.metadata["written"] = True + else: + artifact.metadata["written"] = False + artifacts[kind] = artifact + + return artifacts diff --git a/src/buildcompiler/adapters/pudu/__init__.py b/src/buildcompiler/adapters/pudu/__init__.py new file mode 100644 index 0000000..50838bf --- /dev/null +++ b/src/buildcompiler/adapters/pudu/__init__.py @@ -0,0 +1,16 @@ +"""PUDU adapter exports.""" + +from .assembly_json import assembly_route_to_pudu_json, assembly_routes_to_pudu_json +from .plating_json import plating_to_pudu_json +from .transformation_json import ( + transformation_to_pudu_json, + transformations_to_pudu_json, +) + +__all__ = [ + "assembly_route_to_pudu_json", + "assembly_routes_to_pudu_json", + "transformation_to_pudu_json", + "transformations_to_pudu_json", + "plating_to_pudu_json", +] diff --git a/src/buildcompiler/adapters/pudu/assembly_json.py b/src/buildcompiler/adapters/pudu/assembly_json.py new file mode 100644 index 0000000..9d5fbe8 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/assembly_json.py @@ -0,0 +1,64 @@ +"""In-memory adapter for compiler-level PUDU assembly JSON payloads.""" + +from collections.abc import Sequence + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent + + +def _stable_identifier(identity: str, display_id: str | None) -> str: + return identity or display_id or "" + + +def assembly_route_to_pudu_json( + *, + product_identity: str, + part_plasmids: Sequence[IndexedPlasmid], + backbone: IndexedBackbone, + restriction_enzyme: IndexedReagent, +) -> dict[str, object]: + """Adapt a selected lvl1 route into legacy-compatible assembly JSON keys.""" + + parts_list = [ + _stable_identifier(identity=part.identity, display_id=part.display_id) + for part in part_plasmids + ] + return { + "Product": product_identity, + "Backbone": _stable_identifier( + identity=backbone.identity, display_id=backbone.display_id + ), + "PartsList": parts_list, + "Restriction Enzyme": ( + restriction_enzyme.name + or _stable_identifier( + identity=restriction_enzyme.identity, + display_id=restriction_enzyme.display_id, + ) + ), + } + + +def assembly_routes_to_pudu_json( + *, + product_identities: Sequence[str], + part_plasmid_routes: Sequence[Sequence[IndexedPlasmid]], + backbones: Sequence[IndexedBackbone], + restriction_enzymes: Sequence[IndexedReagent], +) -> list[dict[str, object]]: + """Batch helper for deterministic in-memory assembly JSON payloads.""" + + return [ + assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=part_plasmids, + backbone=backbone, + restriction_enzyme=restriction_enzyme, + ) + for product_identity, part_plasmids, backbone, restriction_enzyme in zip( + product_identities, + part_plasmid_routes, + backbones, + restriction_enzymes, + strict=True, + ) + ] diff --git a/src/buildcompiler/adapters/pudu/plating_json.py b/src/buildcompiler/adapters/pudu/plating_json.py new file mode 100644 index 0000000..3648e20 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/plating_json.py @@ -0,0 +1,22 @@ +"""In-memory adapter for compiler-level PUDU plating JSON payloads.""" + +from collections import OrderedDict +from collections.abc import Mapping +from typing import Any + + +def plating_to_pudu_json( + *, + bacterium_locations: Mapping[str, str], + advanced_parameters: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Adapt plating records into deterministic legacy-compatible PUDU JSON keys.""" + + stable_locations = OrderedDict( + sorted(bacterium_locations.items(), key=lambda kv: kv[0]) + ) + payload: dict[str, Any] = { + "bacterium_locations": dict(stable_locations), + "advanced_parameters": dict(advanced_parameters or {}), + } + return payload diff --git a/src/buildcompiler/adapters/pudu/transformation_json.py b/src/buildcompiler/adapters/pudu/transformation_json.py new file mode 100644 index 0000000..207a561 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/transformation_json.py @@ -0,0 +1,53 @@ +"""In-memory adapter for compiler-level PUDU transformation JSON payloads.""" + +from collections.abc import Sequence + +from buildcompiler.domain import IndexedPlasmid + + +def _stable_identifier(identity: str, display_id: str | None) -> str: + return identity or display_id or "" + + +def _plasmid_identifier(plasmid: IndexedPlasmid | str) -> str: + if isinstance(plasmid, str): + return plasmid + return _stable_identifier(identity=plasmid.identity, display_id=plasmid.display_id) + + +def transformation_to_pudu_json( + *, + strain_identity: str, + chassis_identity: str, + plasmids: Sequence[IndexedPlasmid | str], +) -> dict[str, object]: + """Adapt a transformation record into legacy-compatible PUDU JSON keys.""" + + return { + "Strain": strain_identity, + "Chassis": chassis_identity, + "Plasmids": [_plasmid_identifier(plasmid) for plasmid in plasmids], + } + + +def transformations_to_pudu_json( + *, + strain_identities: Sequence[str], + chassis_identities: Sequence[str], + plasmid_sets: Sequence[Sequence[IndexedPlasmid | str]], +) -> list[dict[str, object]]: + """Batch helper for deterministic in-memory transformation JSON payloads.""" + + return [ + transformation_to_pudu_json( + strain_identity=strain_identity, + chassis_identity=chassis_identity, + plasmids=plasmids, + ) + for strain_identity, chassis_identity, plasmids in zip( + strain_identities, + chassis_identities, + plasmid_sets, + strict=True, + ) + ] diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py new file mode 100644 index 0000000..fc04f2b --- /dev/null +++ b/src/buildcompiler/api/__init__.py @@ -0,0 +1,34 @@ +"""Public API contracts, options, and compiler facade for BuildCompiler.""" + +from .compiler import BuildCompiler, full_build +from .options import ( + ApprovalOptions, + BuildOptions, + CombinatorialOptions, + DomesticationOptions, + ExecutionOptions, + Lvl2SearchOptions, + PlanningOptions, + ProtocolMode, + ProtocolOptions, + ReagentOptions, + ReportingOptions, + SelectionOptions, +) + +__all__ = [ + "BuildCompiler", + "ApprovalOptions", + "BuildOptions", + "CombinatorialOptions", + "DomesticationOptions", + "ExecutionOptions", + "Lvl2SearchOptions", + "PlanningOptions", + "ProtocolMode", + "ProtocolOptions", + "ReagentOptions", + "ReportingOptions", + "SelectionOptions", + "full_build", +] diff --git a/src/buildcompiler/api/compiler.py b/src/buildcompiler/api/compiler.py new file mode 100644 index 0000000..2a82db0 --- /dev/null +++ b/src/buildcompiler/api/compiler.py @@ -0,0 +1,117 @@ +"""Public BuildCompiler API skeleton.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from buildcompiler.planning import FullBuildPlanner + +from .options import BuildOptions + + +@dataclass +class BuildCompiler: + inventory: Any = None + sbol_document: Any = None + planner: Any = None + executor: Any = None + adapters: Any = None + options: BuildOptions = field(default_factory=BuildOptions) + + @classmethod + def from_synbiohub( + cls, + *, + collections: list[str] | None = None, + sbh_registry: str | None = None, + auth_token: str | None = None, + sbol_doc: Any = None, + options: BuildOptions | None = None, + **kwargs: Any, + ) -> "BuildCompiler": + if collections: + raise NotImplementedError( + "Automatic SynBioHub collection loading/indexing is not implemented yet. Inject inventory dependencies directly for now." + ) + return cls(sbol_document=sbol_doc, options=options or BuildOptions(), **kwargs) + + def plan(self, abstract_designs: Any, options: BuildOptions | None = None) -> Any: + effective_options = options or self.options + planner = self.planner or FullBuildPlanner(options=effective_options) + return planner.plan(abstract_designs, options=effective_options) + + def execute(self, plan: Any, options: BuildOptions | None = None) -> Any: + effective_options = options or self.options + executor = self.executor + if executor is None: + if self.inventory is None: + raise ValueError( + "BuildCompiler.execute requires an inventory when no executor is injected." + ) + if self.sbol_document is None: + raise ValueError( + "BuildCompiler.execute requires an sbol_document when no executor is injected." + ) + from buildcompiler.execution import FullBuildExecutor + + executor = FullBuildExecutor.from_dependencies( + inventory=self.inventory, + sbol_document=self.sbol_document, + options=effective_options, + adapters=self.adapters, + ) + return executor.execute(plan, options=effective_options) + + def full_build( + self, abstract_designs: Any, options: BuildOptions | None = None + ) -> Any: + plan = self.plan(abstract_designs, options=options) + return self.execute(plan, options=options) + + +def full_build( + abstract_designs: Any, + *, + inventory: Any = None, + sbol_document: Any = None, + planner: Any = None, + executor: Any = None, + adapters: Any = None, + options: BuildOptions | None = None, + collections: list[str] | None = None, + sbh_registry: str | None = None, + auth_token: str | None = None, + sbol_doc: Any = None, + **kwargs: Any, +) -> Any: + compiler_options = options or BuildOptions() + if ( + collections is not None + or sbh_registry is not None + or auth_token is not None + or sbol_doc is not None + ): + compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, + options=compiler_options, + inventory=inventory, + planner=planner, + executor=executor, + adapters=adapters, + **kwargs, + ) + else: + compiler = BuildCompiler( + inventory=inventory, + sbol_document=sbol_document, + planner=planner, + executor=executor, + adapters=adapters, + options=compiler_options, + **kwargs, + ) + return compiler.full_build(abstract_designs, options=compiler_options) diff --git a/src/buildcompiler/api/options.py b/src/buildcompiler/api/options.py new file mode 100644 index 0000000..5e62868 --- /dev/null +++ b/src/buildcompiler/api/options.py @@ -0,0 +1,89 @@ +"""Build options contracts for full_build configuration.""" + +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Literal + + +class ProtocolMode(str, Enum): + """Protocol generation mode.""" + + NONE = "none" + MANUAL = "manual" + AUTOMATED = "automated" + + +@dataclass +class CombinatorialOptions: + max_variants: int = 256 + allow_large_expansion: bool = False + + +@dataclass +class Lvl2SearchOptions: + max_exhaustive_region_count: int = 4 + allow_large_order_search: bool = False + + +@dataclass +class PlanningOptions: + combinatorial: CombinatorialOptions = field(default_factory=CombinatorialOptions) + lvl2_search: Lvl2SearchOptions = field(default_factory=Lvl2SearchOptions) + + +@dataclass +class ExecutionOptions: + max_iterations: int = 5 + continue_on_error: bool = False + + +@dataclass +class SelectionOptions: + prefer_existing_collection_material: bool = True + prefer_higher_material_state: bool = True + + +@dataclass +class ProtocolOptions: + mode: ProtocolMode = ProtocolMode.NONE + simulate: bool = False + results_dir: str | Path | None = None + + +@dataclass +class ReportingOptions: + include_detailed_report: bool = False + include_rejected_routes: bool = True + max_rejected_routes: int = 3 + + +@dataclass +class ApprovalOptions: + approved_processes: set[str] = field(default_factory=set) + approved_approval_ids: set[str] = field(default_factory=set) + scope: Literal["run", "persistent"] = "run" + + +@dataclass +class ReagentOptions: + allow_reagent_purchase: bool = False + default_restriction_enzyme: str = "BsaI" + default_ligase: str = "T4_DNA_ligase" + + +@dataclass +class DomesticationOptions: + allow_sequence_domestication_edits: bool = False + + +@dataclass +class BuildOptions: + planning: PlanningOptions = field(default_factory=PlanningOptions) + execution: ExecutionOptions = field(default_factory=ExecutionOptions) + selection: SelectionOptions = field(default_factory=SelectionOptions) + protocol: ProtocolOptions = field(default_factory=ProtocolOptions) + reporting: ReportingOptions = field(default_factory=ReportingOptions) + approvals: ApprovalOptions = field(default_factory=ApprovalOptions) + reagents: ReagentOptions = field(default_factory=ReagentOptions) + domestication: DomesticationOptions = field(default_factory=DomesticationOptions) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 241b67c..e1de21e 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,24 +1,53 @@ -import random import sbol2 -from typing import List, Dict +import json +import random +import re +import shutil +import warnings +from typing import Any, List, Dict, Tuple +import urllib.parse +import csv +from pathlib import Path from buildcompiler.plasmid import Plasmid -from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence +from buildcompiler.sbol2build import ( + Assembly, + Transformation as SBOL2Transformation, + dna_componentdefinition_with_sequence, + rebase_restriction_enzyme, +) from .abstract_translator import ( + enumerate_design_variants, + extract_combinatorial_design_parts, + extract_toplevel_definition, get_or_pull, get_compatible_plasmids, ) +from .robotutils import ( + assembly_plan_RDF_to_JSON, + generate_96_well_positions, + normalize_plating_input, + run_opentrons_script_to_zip, + write_manual_plating_protocol, + write_plate_map_csv, + write_plate_map_json, + write_plating_protocol_script, +) from .constants import ( AMP, + ENGINEERED_REGION, KAN, FUSION_SITES, LIGASE, + LVL2_FUSION_SITE_ORDER, PART_ROLES, + PLASMID_VECTOR, RESTRICTION_ENZYME, RESTRICTION_ENZYME_ASSEMBLY_SCAR, ENGINEERED_PLASMID, PLASMID_CLONING_VECTOR, ORGANISM_STRAIN, + PLATING_ACTIVITY_ROLE, ) @@ -40,18 +69,48 @@ def __init__( collections: List[str], sbh_registry: str, auth_token: str, - sbol_doc: sbol2.Document, + sbol_doc: sbol2.Document = None, + server_mode: bool = False, ): self.sbh = sbol2.PartShop(sbh_registry) self.sbh.key = auth_token self.sbol_doc = sbol_doc or sbol2.Document() self.indexed_plasmids = [] self.indexed_backbones = [] - self.restriction_enzyme_implementations = [] - self.ligase_implementations = [] + self.BsaI_impl = None + self.BbsI_impl = None + self.T4_ligase_impl = None + self.server_mode = server_mode self._index_collections(collections) + @classmethod + def from_local_documents( + cls, + collection_docs: list[sbol2.Document], + design_doc: sbol2.Document | None = None, + ): + """Create a BuildCompiler instance from already-loaded local SBOL documents.""" + compiler = cls.__new__(cls) + compiler.sbh = None + compiler.sbol_doc = sbol2.Document() + compiler.indexed_plasmids = [] + compiler.indexed_backbones = [] + compiler.restriction_enzyme_implementations = [] + compiler.ligase_implementations = [] + + if design_doc is not None: + compiler.index_document(design_doc) + + for collection_doc in collection_docs: + compiler.index_document(collection_doc) + + return compiler + + def index_document(self, collection_doc: sbol2.Document): + self._merge_document(collection_doc) + self._index_document_objects(collection_doc) + def _index_collections(self, collections: List[str]): """Index input collections into plasmids and backbones. @@ -65,11 +124,43 @@ def _index_collections(self, collections: List[str]): :rtype: None """ for uri in collections: + if self.server_mode: + canonical_resource = self.sbh.resource.replace("://api.", "://") + uri = uri.replace(canonical_resource, self.sbh.resource) print(f"Indexing collection: {uri}") self.sbh.pull(uri, self.sbol_doc) + self._index_current_document() + + def _merge_document(self, source_doc: sbol2.Document): + try: + self.sbol_doc.appendString(source_doc.writeString()) + except RuntimeError as exc: + if "SBOL_ERROR_URI_NOT_UNIQUE" in str(exc): + for top_level in source_doc.SBOLObjects.values(): + if top_level.identity in self.sbol_doc: + continue + self.sbol_doc.add(top_level.copy()) + else: + raise + + def _resolve_object(self, uri: str): + existing = self.sbol_doc.find(uri) + if existing is not None: + return existing + if self.sbh is None: + raise ValueError( + f"Referenced SBOL object not found in local documents: {uri}. " + "Local mode does not pull from SynBioHub." + ) + return get_or_pull(self.sbol_doc, self.sbh, uri) + + def _index_current_document(self): + self._index_document_objects(self.sbol_doc) for implementation in self.sbol_doc.implementations: - built_object = get_or_pull(self.sbol_doc, self.sbh, implementation.built) + built_object = get_or_pull( + self.sbol_doc, self.sbh, implementation.built, self.server_mode + ) if ( type(built_object) is sbol2.ModuleDefinition and ORGANISM_STRAIN in built_object.roles @@ -86,7 +177,9 @@ def _index_collections(self, collections: List[str]): self.indexed_plasmids, built_object ) if existing_plasmid: - existing_plasmid.plasmid_implementations.append(implementation) + self._append_implementation_once( + existing_plasmid.plasmid_implementations, implementation + ) else: self.indexed_plasmids.append( Plasmid( @@ -98,7 +191,9 @@ def _index_collections(self, collections: List[str]): self.indexed_backbones, built_object ) if existing_backbone: - existing_backbone.plasmid_implementations.append(implementation) + self._append_implementation_once( + existing_backbone.plasmid_implementations, implementation + ) else: self.indexed_backbones.append( Plasmid( @@ -107,15 +202,24 @@ def _index_collections(self, collections: List[str]): ) elif sbol2.BIOPAX_PROTEIN in built_object.types: if RESTRICTION_ENZYME in built_object.roles: - self.restriction_enzyme_implementations.append(implementation) + if ( + "http://rebase.neb.com/rebase/enz/BsaI.html" + in built_object.wasDerivedFrom + ): + self.BsaI_impl = implementation + elif ( + "http://rebase.neb.com/rebase/enz/BbsI.html" + in built_object.wasDerivedFrom + ): + self.BbsI_impl = implementation elif LIGASE in built_object.roles: - self.ligase_implementations.append(implementation) + self.T4_ligase_impl = implementation - for strain in self.sbol_doc.moduleDefinitions: + for strain in source_doc.moduleDefinitions: if ORGANISM_STRAIN in strain.roles: self._extract_plasmids_from_strain(strain, None, self.sbol_doc) - for definition in self.sbol_doc.componentDefinitions: + for definition in source_doc.componentDefinitions: self._sort_plasmid_components(definition, self.sbol_doc) def domestication( @@ -156,25 +260,18 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: removals += 1 return domesticated_sequence, removals - bsaI_impl = next( - ( - impl - for impl in self.restriction_enzyme_implementations - if self.sbol_doc.find(impl.built).displayId == "BsaI" - ), - None, - ) - if bsaI_impl is None: - raise ValueError( - "BsaI Restriction enzyme not found in provided collections. Terminating domestication." + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, ) - ligase_impl = ( - self.ligase_implementations[0] if self.ligase_implementations else None - ) - if ligase_impl is None: - raise ValueError( - "No appropriate ligase found in provided collections. Terminating domestication." + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, ) dsDNAs = [] @@ -263,8 +360,8 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: assembly = Assembly( [Plasmid(insert_definition, None, [insert_impl], [], self.sbol_doc)], backbone, - bsaI_impl, - ligase_impl, + self.BsaI_impl, + self.T4_ligase_impl, self.sbol_doc, ) assembly_products, assembly_doc = assembly.run() @@ -274,8 +371,14 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: return domesticated_parts def assembly_lvl1( - self, abstract_design: sbol2.ComponentDefinition, backbone: Plasmid = None - ) -> list[sbol2.ComponentDefinition]: + self, + abstract_designs: ( + List[sbol2.ComponentDefinition] | sbol2.CombinatorialDerivation + ), + final_doc: sbol2.Document = sbol2.Document(), + product_name: str = "composite", + backbone: Plasmid | Dict[str, Plasmid] | None = None, + ) -> Tuple[Dict, sbol2.Document]: """Assemble level-1 plasmids for each gene/transcriptional unit. Uses indexed plasmids/backbones and the current design to assemble @@ -286,56 +389,134 @@ def assembly_lvl1( :raises LookupError: If compatible plasmids or backbones cannot be found. """ - # TODO: Identify parts from the abstract design needed for lvl1 assembly and find compatible indexed plasmids/backbones. - # if backbone provided then use it.Then look for parts constraind by the backbone fusion sites. - # else, run an algorithm to try a backbone from 4 the choices. If it fails on the 4 raise an error. - - plasmid_dict = self._get_input_plasmids( - design=abstract_design, antibiotic_resistance=AMP - ) - - if not backbone: - backbone, compatible_plasmids = self._get_backbone( - plasmid_dict, antibiotic_resistance=KAN + assembly_dict = {} + if type(abstract_designs) is sbol2.CombinatorialDerivation: + abstract_design_def = self.sbol_doc.getComponentDefinition( + abstract_designs.masterTemplate ) - else: - compatible_plasmids = get_compatible_plasmids(plasmid_dict, backbone) - bsaI_impl = next( - impl - for impl in self.restriction_enzyme_implementations - if self.sbol_doc.find(impl.built).displayId == "BsaI" - ) - if bsaI_impl is None: - raise ValueError( - "BsaI Restriction enzyme not found in provided collections. Terminating assembly." + combinatorial_part_dict = self.extract_combinatorial_design_parts( + abstract_design_def, abstract_designs ) - ligase_impl = self.ligase_implementations[0] - if bsaI_impl is None: - raise ValueError( - "No appropriate ligase found in provided collections. Terminating assembly." - ) + enumerated_part_lists = enumerate_design_variants(combinatorial_part_dict) - assembly = Assembly( - compatible_plasmids, backbone, bsaI_impl, ligase_impl, self.sbol_doc - ) - composite_plasmids, product_doc = assembly.run() + for i, list in enumerate(enumerated_part_lists): + plasmid_dict = self._construct_plasmid_dict(list, AMP) - self.indexed_plasmids.extend(composite_plasmids) + if isinstance(backbone, dict): + raise ValueError( + "A backbone dictionary cannot be used with a CombinatorialDerivation. " + "All variants share the same template, so supply a single Plasmid or None to auto-select." + ) + elif not backbone: + backbone, compatible_plasmids = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + elif type(backbone) is Plasmid: + compatible_plasmids = get_compatible_plasmids( + plasmid_dict, backbone + ) + + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) - return composite_plasmids + assembly = Assembly( + compatible_plasmids, + backbone, + self.BsaI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + f"{abstract_design_def.displayId}_{product_name}_comb{i}", + ) + composite_plasmids, final_doc = ( + assembly.run() + ) # TODO upload product_doc? - # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. - # Using he selected parts create the representation, you need Plasmids, BsaI and T4 Ligase. - # TODO: Updates indexed plasmids with assembled versions. - # TODO: Generate a protocol for the assembly process. - protocol = "To be implemented by PUDU" + self.indexed_plasmids.extend( + composite_plasmids + ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time - return protocol + assembly_dict.setdefault(abstract_design_def.identity, []).extend( + composite_plasmids + ) + else: # list of designs + for abstract_design in abstract_designs: + plasmid_dict = self._get_input_plasmids( + design=abstract_design, antibiotic_resistance=AMP + ) + + if not backbone: + resolved_backbone, compatible_plasmids = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + elif isinstance(backbone, dict): + resolved_backbone = backbone.get(abstract_design.displayId) + if resolved_backbone is None: + raise ValueError( + f"Backbone dict provided but no entry found for design '{abstract_design.displayId}'. " + f"Available keys: {list(backbone.keys())}" + ) + compatible_plasmids = get_compatible_plasmids( + plasmid_dict, resolved_backbone + ) + else: + resolved_backbone, compatible_plasmids = ( + backbone, + get_compatible_plasmids(plasmid_dict, backbone), + ) + + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) + + assembly = Assembly( + compatible_plasmids, + resolved_backbone, + self.BsaI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + f"{abstract_design.displayId}_{product_name}", + ) + composite_plasmids, final_doc = ( + assembly.run() + ) # TODO upload product_doc? + + self.indexed_plasmids.extend( + composite_plasmids + ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time + assembly_dict[abstract_design.identity] = composite_plasmids + + return assembly_dict, final_doc def assembly_lvl2( self, + abstract_design_doc: sbol2.Document, + backbone: Plasmid = None, + product_name: str = None, ) -> list[sbol2.ComponentDefinition]: """Assemble level-2 plasmids for the full design. @@ -346,14 +527,480 @@ def assembly_lvl2( :rtype: list[Plasmid] :raises LookupError: If compatible plasmids or backbones cannot be found. """ + # get high level genes, send to assembly_lvl1 + # send original abstract_design to get a new dictionary + # send new dictionary to _get_backbone or get_compatible plasmids with AMP + TUs = _extract_lvl2_TUs(abstract_design_doc) + backbone_dict = {} + lvl1_plasmids = [] + + for i, TU in enumerate(TUs): + print(TU.displayId) + + # l1 backbone zselection + backbone_fusion_sites = LVL2_FUSION_SITE_ORDER[i] + lvl1_backbone = next( + plasmid + for plasmid in self.indexed_backbones + if plasmid.fusion_sites == backbone_fusion_sites + and plasmid.antibiotic_resistance == KAN + ) + + backbone_dict[TU.displayId] = lvl1_backbone + + # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN + + composite_plasmid_dict, final_doc = self.assembly_lvl1( + TUs, backbone=backbone_dict, product_name=f"{TU.displayId}_plas" + ) + + for key, composites in composite_plasmid_dict.items(): + simplified_representation, new_defs = self._encapsulate_TU(composites[0]) + final_doc.add_list(new_defs) + lvl1_plasmids.append(simplified_representation) + print(simplified_representation) + + # get l2 backbone + plasmid_dict = {} + for p in lvl1_plasmids: + key = p.plasmid_definition.displayId + plasmid_dict.setdefault(key, []).append(p) + + if backbone is None: + backbone, _ = self._get_backbone(plasmid_dict, antibiotic_resistance=AMP) + + print(backbone) + + # BbsI for l2 + if self.BbsI_impl is None: + self._create_RE_implementation("BbsI") + warnings.warn( + "BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + # TODO see about making these common enzymes (BsaI, BbSI, T4) global or class variables, so they only need to be searched for once + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) + + assembly = Assembly( + lvl1_plasmids, + backbone, + self.BbsI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + product_name, + ) + + lvl2_plasmids, final_doc = assembly.run() # TODO upload product_doc? + self.indexed_plasmids.extend(lvl2_plasmids) + + return lvl2_plasmids, final_doc + + def transformation( + self, + assembly_products: List[Plasmid], + chassis_name: str = "E_coli_DH5alpha", + transformation_doc: sbol2.Document = None, + ) -> Dict[str, Any]: + """Generate deterministic transformation artifacts from assembly outputs. + + :param assembly_products: Structured inputs produced by an assembly stage + :type assembly_products: List[Plasmid] + :param chassis_name: Display id used for the chassis module and implementation. + :type chassis_name: str + :param transformation_doc: Optional SBOL document to write outputs into. + :type transformation_doc: sbol2.Document | None + :returns: Structured transformation outputs including SBOL references, + robot JSON intermediate, protocol placeholders, and logs. + :rtype: dict + :raises ValueError: If no valid plasmid inputs can be extracted. + """ + if transformation_doc is None: + transformation_doc = self.sbol_doc + + chassis_module, chassis_impl = self._get_or_create_chassis( + transformation_doc, chassis_name + ) + + sbol_outputs = [] + robot_steps = [] + logs = [] + + for index, plasmid_obj in enumerate(assembly_products, start=1): + plasmid = plasmid_obj.plasmid_definition + + if not plasmid_obj.plasmid_implementations: + raise ValueError( + f"No plasmid implementations found for {plasmid.displayId}" + ) + + plasmid_impl = plasmid_obj.plasmid_implementations[0] + + transform_id = f"transform_{plasmid.displayId}_{index}" + + transformation_activity = sbol2.Activity(transform_id) + transformation_activity.name = ( + f"Transform {chassis_name} with {plasmid.displayId}" + ) + transformation_activity.types = "http://sbols.org/v2#build" + + chassis_usage = sbol2.Usage( + uri=f"{transform_id}_chassis", + entity=chassis_impl.identity, + role="http://sbols.org/v2#build", + ) + plasmid_usage = sbol2.Usage( + uri=f"{transform_id}_plasmid", + entity=plasmid_impl.identity, + role="http://sbols.org/v2#build", + ) + transformation_activity.usages = [chassis_usage, plasmid_usage] + + transformed_strain = sbol2.ModuleDefinition( + f"{chassis_name}_with_{plasmid.displayId}" + ) + transformed_strain.roles = [ORGANISM_STRAIN] + transformed_strain.name = ( + f"{chassis_name} transformed with {plasmid.displayId}" + ) + + chassis_module_ref = sbol2.Module( + uri=f"{transformed_strain.displayId}_chassis" + ) + chassis_module_ref.definition = chassis_module.identity + plasmid_fc = sbol2.FunctionalComponent( + uri=f"{transformed_strain.displayId}_plasmid" + ) + plasmid_fc.definition = plasmid.identity + + transformed_strain.modules = [chassis_module_ref] + transformed_strain.functionalComponents = [plasmid_fc] + + transformation_activity_association = sbol2.Association( + f"transform_{chassis_module_ref.name}" + ) + + transformation_activity_plan = sbol2.Plan( + f"{transformed_strain.displayId}_transformation_plan" + ) + transformation_activity_plan.description = ( + "TODO: generate accurate description of transformation" + ) + transformation_activity_association.plan = transformation_activity_plan + + transformation_activity_agent = sbol2.Agent("BuildCompiler") + transformation_activity_association.agent = transformation_activity_agent + + transformation_activity.associations = [transformation_activity_association] + + transformed_impl = sbol2.Implementation( + f"{transformed_strain.displayId}_impl" + ) + + transformed_impl.built = transformed_strain.identity + transformed_impl.wasGeneratedBy = transformation_activity.identity + + for obj in ( + transformation_activity, + chassis_usage, + plasmid_usage, + transformed_strain, + chassis_module_ref, + plasmid_fc, + transformed_impl, + ): + self._add_if_absent(transformation_doc, obj) + + sbol_outputs.append( + { + "transformation_activity": transformation_activity.identity, + "transformed_strain_module": transformed_strain.identity, + "transformed_strain_implementation": transformed_impl.identity, + } + ) + robot_steps.append( + { + "step": index, + "plasmid": plasmid.displayId, + "chassis": chassis_name, + "mix_ul": {"competent_cells": 50, "assembly_product": 5}, + "heat_shock": {"temperature_c": 42, "duration_seconds": 45}, + "recovery": {"medium": "SOC", "volume_ul": 950, "duration_min": 60}, + } + ) + logs.append( + f"Prepared transformation input for plasmid {plasmid.displayId} into chassis {chassis_name}." + ) + + return { + "stage": "transformation", + "inputs": [ + plasmid.plasmid_definition.displayId for plasmid in assembly_products + ], + "chassis": chassis_name, + "sbol_artifacts": sbol_outputs, + "json_intermediate": { + "protocol": "chemical_transformation", + "version": "0.1", + "steps": robot_steps, + }, + "protocol_artifacts": { + "ot2_script": "TODO: adapter to protocol generator", + "human_instructions": [ + "Thaw competent cells on ice.", + "Combine assembly product with competent cells as specified.", + "Run heat shock and recovery according to generated parameters.", + ], + "logs": logs, + }, + } + + def transformation( + self, + assembly_products: List[Any], + chassis_name: str = "E_coli_DH5alpha", + transformation_doc: sbol2.Document = None, + ) -> Dict[str, Any]: + """Generate deterministic transformation artifacts from assembly outputs. + + The method accepts either: + - ``Plasmid`` objects, + - ``sbol2.ComponentDefinition`` plasmids, or + - dictionaries containing at least a ``plasmid`` key with one of the above. + + :param assembly_products: Structured inputs produced by an assembly stage. + :type assembly_products: list + :param chassis_name: Display id used for the chassis module and implementation. + :type chassis_name: str + :param transformation_doc: Optional SBOL document to write outputs into. + :type transformation_doc: sbol2.Document | None + :returns: Structured transformation outputs including SBOL references, + robot JSON intermediate, protocol placeholders, and logs. + :rtype: dict + :raises ValueError: If no valid plasmid inputs can be extracted. + """ + if transformation_doc is None: + transformation_doc = self.sbol_doc - # TODO: Identify parts from the abstract design needed for lvl2 assembly and find compatible indexed plasmids/backbones. - # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. - # TODO: Generate a protocol for the assembly process. - protocol = "To be implemented by PUDU" - # TODO: Updates indexed plasmids with assembled versions. + normalized_products = self._normalize_transformation_inputs(assembly_products) + if not normalized_products: + raise ValueError("transformation requires at least one plasmid input.") - return protocol + chassis_module, chassis_impl = self._get_or_create_chassis( + transformation_doc, chassis_name + ) + normalized_plasmids = [] + for product in normalized_products: + indexed = self._get_indexed_plasmid( + self.indexed_plasmids, product["plasmid"] + ) + if indexed is None: + indexed = type( + "TransformationPlasmid", + (), + { + "plasmid_definition": product["plasmid"], + "plasmid_implementations": [], + "name": product["plasmid"].displayId, + }, + )() + normalized_plasmids.append(indexed) + + sbol_outputs = SBOL2Transformation( + plasmids=normalized_plasmids, + chassis_name=chassis_name, + source_document=transformation_doc, + ).chemical_transformation() + + robot_steps = [] + logs = [] + + for index, product in enumerate(normalized_products, start=1): + plasmid = product["plasmid"] + robot_steps.append( + { + "step": index, + "plasmid": plasmid.displayId, + "chassis": chassis_name, + "mix_ul": {"competent_cells": 50, "assembly_product": 5}, + "heat_shock": {"temperature_c": 42, "duration_seconds": 45}, + "recovery": {"medium": "SOC", "volume_ul": 950, "duration_min": 60}, + } + ) + logs.append( + f"Prepared transformation input for plasmid {plasmid.displayId} into chassis {chassis_name}." + ) + + return { + "stage": "transformation", + "inputs": [item["source"] for item in normalized_products], + "chassis": chassis_name, + "sbol_artifacts": sbol_outputs, + "json_intermediate": { + "protocol": "chemical_transformation", + "version": "0.1", + "steps": robot_steps, + }, + "protocol_artifacts": { + "ot2_script": "TODO: adapter to protocol generator", + "human_instructions": [ + "Thaw competent cells on ice.", + "Combine assembly product with competent cells as specified.", + "Run heat shock and recovery according to generated parameters.", + ], + "logs": logs, + }, + } + + def plating( + self, + transformation_results: dict, + results_dir: str | Path, + protocol_type: str = "manual", + advanced_params: dict | None = None, + plate_name: str | None = None, + plating_doc: sbol2.Document | None = None, + overwrite: bool = False, + ) -> Dict[str, Any]: + """Generate plating layout artifacts and protocol metadata. + + This implementation is file/metadata oriented and does not create new + SBOL objects for plating. + """ + if protocol_type not in {"manual", "automated"}: + raise ValueError("protocol_type must be one of: 'manual', 'automated'.") + advanced_params = advanced_params or {} + doc_ref = plating_doc or self.sbol_doc + + normalized = normalize_plating_input(transformation_results, doc=doc_ref) + if len(normalized) > 96: + raise ValueError("plating supports up to 96 transformed strains.") + + wells = generate_96_well_positions(limit=len(normalized)) + results_path = Path(results_dir) + results_path.mkdir(parents=True, exist_ok=True) + + plate_id = plate_name or "solid_96_well_plate" + plate_rows = [] + plate_map = {} + bacterium_locations = {} + + for idx, entry in enumerate(normalized): + well = wells[idx] + source_impl_uri = entry.get("source_impl_uri") + source_impl = doc_ref.find(source_impl_uri) if source_impl_uri else None + strain_module_uri = entry.get("strain_module_uri") + if strain_module_uri is None and source_impl is not None: + strain_module_uri = getattr(source_impl, "built", None) + + display_source = source_impl_uri or strain_module_uri or f"strain_{idx+1}" + parsed = urllib.parse.urlparse(display_source) + slug = parsed.path.split("/")[-1] if parsed.path else display_source + slug = slug.replace("#", "_").replace(":", "_") + + plated_impl_id = f"{slug}_plated_{well}_impl" + plate_map[well] = plated_impl_id + display_name = plated_impl_id + bacterium_locations[well] = display_name + plate_rows.append( + { + "well": well, + "source_transformed_strain_implementation": source_impl_uri, + "strain_module": strain_module_uri, + "plated_strain_implementation": plated_impl_id, + "strain_display_name": display_name, + } + ) + + plate_layout_csv = results_path / "plate_layout_dataframe.csv" + with plate_layout_csv.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter( + handle, + fieldnames=list(plate_rows[0].keys()) if plate_rows else ["well"], + ) + writer.writeheader() + for row in plate_rows: + writer.writerow(row) + + plate_map_json_path = write_plate_map_json( + results_path / "plate_map.json", + { + "plate_implementation": plate_id, + "protocol_type": protocol_type, + "well_map": plate_rows, + }, + ) + plate_map_csv_path = write_plate_map_csv( + results_path / "plate_map.csv", plate_rows + ) + plating_input_json_path = write_plate_map_json( + results_path / "plating_input.json", + {"bacterium_locations": bacterium_locations}, + ) + + logs = [] + protocol_artifacts: Dict[str, Any] = { + "plate_map_json": str(plate_map_json_path), + "plate_map_csv": str(plate_map_csv_path), + "plate_layout_dataframe_csv": str(plate_layout_csv), + "logs": logs, + "pudu": { + "runner_script": "https://github.com/MyersResearchGroup/PUDU/blob/main/scripts/run_sbol2plating_with_params.py", + "mode": protocol_type, + "advanced_params": advanced_params, + }, + } + + if protocol_type == "manual": + md_path = write_manual_plating_protocol( + results_path / "manual_plating_protocol.md", + plate_id=plate_id, + plate_rows=plate_rows, + advanced_params=advanced_params, + ) + protocol_artifacts["manual_protocol_markdown"] = str(md_path) + else: + script_path = write_plating_protocol_script( + results_path / "plating_ot2.py", + plating_data={"bacterium_locations": bacterium_locations}, + advanced_params=advanced_params, + ) + protocol_artifacts["ot2_script"] = str(script_path) + try: + sim_zip = run_opentrons_script_to_zip( + script_path, + plating_input_json_path, + overwrite=overwrite, + ) + protocol_artifacts["simulation_zip"] = str(sim_zip) + except Exception as exc: + logs.append(f"Opentrons simulation skipped: {exc}") + + return { + "stage": "plating", + "protocol_type": protocol_type, + "plate": { + "plate_implementation": plate_id, + "plate_map": plate_map, + }, + "metadata": { + "plate_rows": plate_rows, + "layout_dataframe_columns": ( + list(plate_rows[0].keys()) if plate_rows else [] + ), + }, + "json_intermediate": { + "plating_data": {"bacterium_locations": bacterium_locations}, + "advanced_params": advanced_params, + }, + "protocol_artifacts": protocol_artifacts, + } def _extract_plasmids_from_strain( self, @@ -363,7 +1010,9 @@ def _extract_plasmids_from_strain( ): # strain_implementation = optional param for plasmid in strain.functionalComponents: - plasmid_definition = get_or_pull(doc, self.sbh, plasmid.definition) + plasmid_definition = get_or_pull( + doc, self.sbh, plasmid.definition, self.server_mode + ) if ENGINEERED_PLASMID in plasmid_definition.roles: existing = self._get_indexed_plasmid( @@ -433,8 +1082,9 @@ def _sort_plasmid_components( self, definition: sbol2.ComponentDefinition, doc: sbol2.Document ): if len(definition.components) > 1: - if ENGINEERED_PLASMID in definition.roles and not self._get_indexed_plasmid( - self.indexed_plasmids, definition + if ( + ENGINEERED_PLASMID in definition.roles + and not self._get_indexed_plasmid(self.indexed_plasmids, definition) ): self.indexed_plasmids.append(Plasmid(definition, None, [], [], doc)) elif ( @@ -467,7 +1117,6 @@ def _get_backbone( for backbone in sorted_backbones: if backbone.antibiotic_resistance == antibiotic_resistance: # check for compatibility - # also, if we find a hit here we may not need to run get_compatible plasmids later, work is already done try: compatible_plasmids = get_compatible_plasmids( plasmid_dict, backbone @@ -497,12 +1146,56 @@ def _extract_design_parts( """ component_list = [c for c in design.getInSequentialOrder()] return [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + get_or_pull(self.sbol_doc, self.sbh, component.definition, self.server_mode) for component in component_list ] + def extract_combinatorial_design_parts( + self, + design: sbol2.ComponentDefinition, + derivation: sbol2.CombinatorialDerivation, + ) -> Dict[str, List[sbol2.ComponentDefinition]]: + """ + Extracts and returns a mapping of component definitions from a combinatorial design, in order. + Variants of combinatinatorial components are entered in a list corresponding to the URI of the component in the abstract design. + + Args: + design: + The top-level :class:`sbol2.ComponentDefinition` representing the + abstract design template whose components should be extracted in + sequential order. + + derivation: + The :class:`sbol2.CombinatorialDerivation` associated with ``design`` + that defines variable components and their allowed variants. + + Returns: + Dict[str, List[sbol2.ComponentDefinition]]: + A dictionary mapping component identities to lists + of variable component definitions. + + - Sequential design components map to lists containing a single definition. + - Combinatorial variable components map to lists of variant definitions. + """ + component_list = [c for c in design.getInSequentialOrder()] + component_dict = { + component.identity: [ + get_or_pull( + self.sbol_doc, self.sbh, component.definition, self.server_mode + ) + ] + for component in component_list + } + + for component in derivation.variableComponents: + component_dict[component.variable] = [ + self.sbol_doc.getComponentDefinition(var) for var in component.variants + ] + + return component_dict + def _get_abstract_design(self) -> sbol2.ComponentDefinition: - for definition in self.sbol_doc.componentDefinitions: + for definition in source_doc.componentDefinitions: if ( ENGINEERED_PLASMID in definition.roles or PLASMID_CLONING_VECTOR in definition.roles @@ -511,7 +1204,9 @@ def _get_abstract_design(self) -> sbol2.ComponentDefinition: continue component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + get_or_pull( + self.sbol_doc, self.sbh, component.definition, self.server_mode + ) for component in definition.getInSequentialOrder() ] if any( @@ -573,7 +1268,7 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return False else: component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, comp.definition) + get_or_pull(self.sbol_doc, self.sbh, comp.definition, self.server_mode) for comp in plasmid.getInSequentialOrder() ] @@ -591,3 +1286,335 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return True return False + + def _encapsulate_TU( + self, plasmid: Plasmid + ) -> Tuple[Plasmid, List[sbol2.Identified]]: + """ + Collapse a detailed plasmid with a transcriptional unit (pro, rbs, cds, terminator) + into a simplified representation: + + fusion_site_left -> TU -> fusion_site_right -> backbone + + Builds new sequences for both the TU and simplified plasmid. + + Returns + ------- + Tuple[Plasmid, List[Identified]] + simplified plasmid and all new SBOL objects created + """ + + new_objs = [] + plasmid_def = plasmid.plasmid_definition + + fusion_left, fusion_right = plasmid.fusion_sites + left_seq = FUSION_SITES[fusion_left] + right_seq = FUSION_SITES[fusion_right] + + left_def = None + right_def = None + backbone_def = None + promoter = None + terminator = None + + # scan subcomponents for pro and term to establish range + get backbone + for comp in plasmid_def.components: + comp_def = self.sbol_doc.get(comp.definition) + + if RESTRICTION_ENZYME_ASSEMBLY_SCAR in comp_def.roles: + seq_obj = self.sbol_doc.get(comp_def.sequences[0]) + if seq_obj.elements == left_seq: + left_def = comp_def + continue + if seq_obj.elements == right_seq: + right_def = comp_def + continue + + elif PLASMID_VECTOR in comp_def.roles: + backbone_def = comp_def + + elif sbol2.SO_PROMOTER in comp_def.roles: + promoter = comp + + elif sbol2.SO_TERMINATOR in comp_def.roles: + terminator = comp + + if promoter is None or terminator is None: + raise ValueError("Could not locate promoter or terminator in plasmid TU") + + comp_dict = {c.identity: c for c in plasmid_def.components} + + follows = {} + for sc in plasmid_def.sequenceConstraints: + if sc.restriction == sbol2.SBOL_RESTRICTION_PRECEDES: + subject_comp = comp_dict[sc.subject] + object_comp = comp_dict[sc.object] + follows[subject_comp] = object_comp + + old_tu_components = [] + curr_comp = promoter + + while True: + old_tu_components.append(curr_comp) + + if curr_comp.identity == terminator.identity: + break + + if curr_comp not in follows: + raise ValueError("Broken sequence constraint chain in TU") + + curr_comp = follows[curr_comp] + + def build_sequence_from_components(components): + seq = "" + ranges = {} + cursor = 1 + + for comp in components: + comp_def = self.sbol_doc.get(comp.definition) + + if not comp_def.sequences: + raise ValueError(f"{comp_def.displayId} has no sequence") + + seq_obj = self.sbol_doc.get(comp_def.sequences[0]) + part_seq = seq_obj.elements + + start = cursor + end = cursor + len(part_seq) - 1 + + ranges[comp.identity] = (start, end) + + seq += part_seq + cursor = end + 1 + + return seq, ranges + + # Create TU definition + tu_def = sbol2.ComponentDefinition(plasmid_def.displayId + "_TU") + tu_def.roles = [ENGINEERED_REGION] + + self.sbol_doc.add(tu_def) + new_objs.append(tu_def) + + # map old components to new + comp_map = {} + + for comp in old_tu_components: + new_comp = tu_def.components.create(comp.displayId) + new_comp.definition = comp.definition + comp_map[comp.identity] = new_comp.identity + + # Build TU sequence + tu_seq_string, tu_ranges = build_sequence_from_components(old_tu_components) + + tu_seq = sbol2.Sequence( + tu_def.displayId + "_seq", + elements=tu_seq_string, + encoding=sbol2.SBOL_ENCODING_IUPAC, + ) + + self.sbol_doc.addSequence(tu_seq) + tu_def.sequences = [tu_seq.identity] + + new_objs.append(tu_seq) + + # Copy TU annotations + for sa in plasmid_def.sequenceAnnotations: + if sa.component not in comp_map: + continue + + new_sa = tu_def.sequenceAnnotations.create(sa.displayId) + new_sa.component = comp_map[sa.component] + + offset_start, _ = tu_ranges[sa.component] + + for loc in sa.locations: + if isinstance(loc, sbol2.Range): + new_start = offset_start + loc.start - 1 + new_end = offset_start + loc.end - 1 + + new_loc = new_sa.locations.createRange(loc.displayId) + new_loc.start = new_start + new_loc.end = new_end + new_loc.orientation = loc.orientation + + # -------------------------------------------------- + # Copy TU sequence constraints + # -------------------------------------------------- + for sc in plasmid_def.sequenceConstraints: + if sc.subject in comp_map and sc.object in comp_map: + new_sc = tu_def.sequenceConstraints.create(sc.displayId) + new_sc.subject = comp_map[sc.subject] + new_sc.object = comp_map[sc.object] + new_sc.restriction = sc.restriction + + # -------------------------------------------------- + # Build simplified plasmid definition + # -------------------------------------------------- + simple_plasmid_def = sbol2.ComponentDefinition( + plasmid_def.displayId + "_simple" + ) + + self.sbol_doc.addComponentDefinition(simple_plasmid_def) + new_objs.append(simple_plasmid_def) + + simple_plasmid_def.types = list(plasmid_def.types) + simple_plasmid_def.roles = list(plasmid_def.roles) + + fusion_left_comp = simple_plasmid_def.components.create("fusion_left") + fusion_left_comp.definition = left_def.identity + + tu_comp = simple_plasmid_def.components.create("TU") + tu_comp.definition = tu_def.identity + + fusion_right_comp = simple_plasmid_def.components.create("fusion_right") + fusion_right_comp.definition = right_def.identity + + backbone_comp = None + if backbone_def: + backbone_comp = simple_plasmid_def.components.create("backbone") + backbone_comp.definition = backbone_def.identity + + # -------------------------------------------------- + # Sequence ordering constraints + # -------------------------------------------------- + constraint_counter = 0 + + def add_precedes(subj, obj): + nonlocal constraint_counter + sc = simple_plasmid_def.sequenceConstraints.create( + f"constraint_{constraint_counter}" + ) + sc.subject = subj.identity + sc.object = obj.identity + sc.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + constraint_counter += 1 + + add_precedes(fusion_left_comp, tu_comp) + add_precedes(tu_comp, fusion_right_comp) + + if backbone_comp: + add_precedes(fusion_right_comp, backbone_comp) + + # -------------------------------------------------- + # Build simplified plasmid sequence + # -------------------------------------------------- + ordered_components = [fusion_left_comp, tu_comp, fusion_right_comp] + + if backbone_comp: + ordered_components.append(backbone_comp) + + plas_seq_string, plas_ranges = build_sequence_from_components( + ordered_components + ) + + plas_seq = sbol2.Sequence( + simple_plasmid_def.displayId + "_seq", + elements=plas_seq_string, + encoding=sbol2.SBOL_ENCODING_IUPAC, + ) + + self.sbol_doc.addSequence(plas_seq) + simple_plasmid_def.sequences = [plas_seq.identity] + + new_objs.append(plas_seq) + + for comp_uri, (start, end) in plas_ranges.items(): + anno = simple_plasmid_def.sequenceAnnotations.create( + f"simple_plasmid_def_{start}_{end}_annotation" + ) + anno.component = comp_uri + + location = anno.locations.createRange( + f"{simple_plasmid_def.displayId}_{start}_{end}_location" + ) + location.start = start + location.end = end + + # -------------------------------------------------- + # Construct new plasmid object + # -------------------------------------------------- + new_plasmid = Plasmid( + simple_plasmid_def, + plasmid.strain_definitions[0], + plasmid.plasmid_implementations, + plasmid.strain_implementations, + self.sbol_doc, + ) + + return new_plasmid, new_objs + + def _create_RE_implementation(self, name: str): + RE_def = rebase_restriction_enzyme(name) + + RE_sourcing = sbol2.Activity(f"{name}_restriction_enzyme_purchase") + RE_sourcing.name = "Restriction Enzyme Purchase" + + RE_impl = sbol2.Implementation(f"{RE_def.displayId}_impl") + + RE_impl.built = RE_def.identity + RE_impl.wasGeneratedBy = RE_sourcing.identity + + self.sbol_doc.add_list([RE_impl, RE_def]) + + if name == "BsaI": + self.BsaI_impl = RE_impl + elif name == "BbsI": + self.BbsI_impl = RE_impl + + def _create_ligase_implementation(self): + ligase_def = sbol2.ComponentDefinition("T4_Ligase") + ligase_def.name = "T4_Ligase" + ligase_def.types = [sbol2.BIOPAX_PROTEIN] + ligase_def.roles = ["http://identifiers.org/ncit/NCIT:C16796"] + + ligase_sourcing = sbol2.Activity("ligase_purchase") + ligase_sourcing.name = "Ligase Purchase" + + T4_impl = sbol2.Implementation(f"{ligase_def.displayId}_impl") + + T4_impl.built = ligase_def.identity + T4_impl.wasGeneratedBy = ligase_sourcing.identity + + self.sbol_doc.add_list([T4_impl, ligase_def]) + self.T4_ligase_impl = T4_impl + + def _add_if_absent(self, doc: sbol2.Document, obj: Any): + if doc.find(obj.identity) is None: + doc.add(obj) + + def _get_or_create_chassis( + self, doc: sbol2.Document, chassis_name: str + ) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: + chassis_module = doc.find(chassis_name) or sbol2.ModuleDefinition(chassis_name) + chassis_module.roles = [ORGANISM_STRAIN] + chassis_module.name = chassis_name + self._add_if_absent(doc, chassis_module) + + chassis_impl_id = f"{chassis_name}_impl" + chassis_impl = doc.find(chassis_impl_id) or sbol2.Implementation( + chassis_impl_id + ) + chassis_impl.built = chassis_module.identity + self._add_if_absent(doc, chassis_impl) + return chassis_module, chassis_impl + + +def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler.py? + design_doc: sbol2.Document, +) -> List[sbol2.ComponentDefinition]: + """ + Returns the component definitions of each level-1 component (TU) + in the design. + + Args: + design: :class:`sbol2.Document` containing the design. + + Returns: + A list of TU component definitions in sequential order. + """ + top_design = extract_toplevel_definition(design_doc) + + return [ + design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder() + ] diff --git a/src/buildcompiler/constants.py b/src/buildcompiler/constants.py index 19c25a2..85d047c 100644 --- a/src/buildcompiler/constants.py +++ b/src/buildcompiler/constants.py @@ -23,18 +23,22 @@ "kan": KAN, "amp": AMP, } -# TODO http or https for identifiers? + +LVL2_FUSION_SITE_ORDER = [["A", "E"], ["E", "F"], ["F", "G"], ["G", "H"]] + +# TODO CHANGE ALL TO HTTP (MAKE SURE REFERENCED PARTS ON SBH ARE ALSO HTTP FIRST) ENGINEERED_PLASMID = "http://identifiers.org/so/SO:0000637" ENGINEERED_INSERT = "https://identifiers.org/so/SO:0000915" ENGINEERED_REGION = "http://identifiers.org/so/SO:0000804" -PLASMID_VECTOR = "https://identifiers.org/so/SO:0000755" +PLASMID_VECTOR = "http://identifiers.org/so/SO:0000755" PLASMID_CLONING_VECTOR = "https://identifiers.org/ncit/NCIT:C1919" ANTIBIOTIC_RESISTANCE = "https://identifiers.org/ncit/NCIT:C17449" LIGASE = "http://identifiers.org/ncit/NCIT:C16796" RESTRICTION_ENZYME = "http://identifiers.org/obi/OBI_0000732" RESTRICTION_ENZYME_ASSEMBLY_SCAR = "http://identifiers.org/so/SO:0001953" ORGANISM_STRAIN = "https://identifiers.org/ncit/NCIT:C14419" +PLATING_ACTIVITY_ROLE = "https://w3id.org/buildcompiler/roles/plating_input" FIVE_PRIME_OVERHANG = "http://identifiers.org/so/SO:0001932" THREE_PRIME_OVERHANG = "http://identifiers.org/so/SO:0001933" diff --git a/src/buildcompiler/domain/__init__.py b/src/buildcompiler/domain/__init__.py new file mode 100644 index 0000000..72eacbb --- /dev/null +++ b/src/buildcompiler/domain/__init__.py @@ -0,0 +1,34 @@ +"""Domain contracts for BuildCompiler clean architecture.""" + +from .approvals import ApprovalStatus, RequiredApproval +from .build_request import BuildRequest +from .build_result import FullBuildResult, StageResult +from .build_stage import BuildStage +from .design import DesignKind +from .material_state import MaterialState +from .missing_input import MissingBuildInput +from .plasmid import IndexedBackbone, IndexedPlasmid +from .reagent import IndexedReagent +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + +__all__ = [ + "ApprovalStatus", + "BuildRequest", + "BuildResult", + "BuildStage", + "BuildStatus", + "BuildWarning", + "DesignKind", + "FullBuildResult", + "IndexedBackbone", + "IndexedPlasmid", + "IndexedReagent", + "MaterialState", + "MissingBuildInput", + "RequiredApproval", + "StageResult", + "StageStatus", +] + +BuildResult = FullBuildResult diff --git a/src/buildcompiler/domain/approvals.py b/src/buildcompiler/domain/approvals.py new file mode 100644 index 0000000..2ebbd45 --- /dev/null +++ b/src/buildcompiler/domain/approvals.py @@ -0,0 +1,22 @@ +"""Approval contracts for expected gated processes.""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + + +class ApprovalStatus(str, Enum): + """Minimal approval state used by RequiredApproval.""" + + REQUIRED = "required" + APPROVED = "approved" + + +@dataclass +class RequiredApproval: + """Approval record for a process required to proceed.""" + + status: ApprovalStatus + process: str + reason: str + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_request.py b/src/buildcompiler/domain/build_request.py new file mode 100644 index 0000000..8bcd9c4 --- /dev/null +++ b/src/buildcompiler/domain/build_request.py @@ -0,0 +1,21 @@ +"""Build request contract dataclass.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage +from .design import DesignKind + + +@dataclass +class BuildRequest: + """Planner-produced request for a single stage/source item.""" + + id: str + stage: BuildStage + source_identity: str + source_display_id: str | None + source_kind: DesignKind + parent_group: str | None = None + variant_index: int | None = None + constraints: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_result.py b/src/buildcompiler/domain/build_result.py new file mode 100644 index 0000000..bc252cb --- /dev/null +++ b/src/buildcompiler/domain/build_result.py @@ -0,0 +1,50 @@ +"""Stage/full-build result contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .approvals import RequiredApproval +from .build_stage import BuildStage +from .missing_input import MissingBuildInput +from .plasmid import IndexedPlasmid +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + + +@dataclass +class StageResult: + """Output contract from a single stage invocation.""" + + id: str + stage: BuildStage + status: StageStatus + request_ids: list[str] = field(default_factory=list) + products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + sbol_document: Any | None = None + json_intermediate: dict[str, Any] | list[Any] | None = None + protocol_artifacts: dict[str, Any] = field(default_factory=dict) + logs: list[str] = field(default_factory=list) + + +@dataclass +class FullBuildResult: + """Aggregate full-build output contract. + + Neighboring contracts (plan/graph/summary/report) are intentionally typed + conservatively until their milestone implementations are added. + """ + + status: BuildStatus + plan: Any + build_document: Any + stage_results: list[StageResult] = field(default_factory=list) + graph: Any = None + final_products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + summary: Any = None + report: Any | None = None diff --git a/src/buildcompiler/domain/build_stage.py b/src/buildcompiler/domain/build_stage.py new file mode 100644 index 0000000..a3b52a9 --- /dev/null +++ b/src/buildcompiler/domain/build_stage.py @@ -0,0 +1,13 @@ +"""Build stage enum contracts.""" + +from enum import Enum + + +class BuildStage(str, Enum): + """Planned v1 build stages for full-build execution.""" + + DOMESTICATION = "domestication" + ASSEMBLY_LVL1 = "assembly_lvl1" + ASSEMBLY_LVL2 = "assembly_lvl2" + TRANSFORMATION = "transformation" + PLATING = "plating" diff --git a/src/buildcompiler/domain/design.py b/src/buildcompiler/domain/design.py new file mode 100644 index 0000000..c11bedc --- /dev/null +++ b/src/buildcompiler/domain/design.py @@ -0,0 +1,12 @@ +"""Design identity/type contracts.""" + +from enum import Enum + + +class DesignKind(str, Enum): + """Supported SBOL design source kinds for build requests.""" + + COMPONENT_DEFINITION = "component_definition" + MODULE_DEFINITION = "module_definition" + COMBINATORIAL_DERIVATION = "combinatorial_derivation" + UNSUPPORTED = "unsupported" diff --git a/src/buildcompiler/domain/material_state.py b/src/buildcompiler/domain/material_state.py new file mode 100644 index 0000000..ea5b08e --- /dev/null +++ b/src/buildcompiler/domain/material_state.py @@ -0,0 +1,13 @@ +"""Material lifecycle states used across stages.""" + +from enum import Enum + + +class MaterialState(str, Enum): + """Normalized lifecycle states for build materials.""" + + PLANNED = "planned" + GENERATED = "generated" + ASSEMBLED = "assembled" + TRANSFORMED = "transformed" + PLATED = "plated" diff --git a/src/buildcompiler/domain/missing_input.py b/src/buildcompiler/domain/missing_input.py new file mode 100644 index 0000000..1210eb9 --- /dev/null +++ b/src/buildcompiler/domain/missing_input.py @@ -0,0 +1,32 @@ +"""Missing input/blocker contracts.""" + +from dataclasses import dataclass, field +from typing import Literal + +from .build_stage import BuildStage + +MissingKind = Literal[ + "engineered_region", + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + "reagent", +] + + +@dataclass +class MissingBuildInput: + """Expected blocker produced when build inputs are unavailable.""" + + source_stage: BuildStage + source_design_identity: str + missing_identity: str + missing_display_id: str | None + missing_kind: MissingKind + required_stage: BuildStage | Literal["fatal"] + reason: str + candidates_tried: list[str] = field(default_factory=list) diff --git a/src/buildcompiler/domain/plasmid.py b/src/buildcompiler/domain/plasmid.py new file mode 100644 index 0000000..3a2e0f7 --- /dev/null +++ b/src/buildcompiler/domain/plasmid.py @@ -0,0 +1,30 @@ +"""Normalized plasmid/backbone records.""" + +from dataclasses import dataclass, field +from typing import Any + +from .material_state import MaterialState + + +@dataclass +class IndexedPlasmid: + """Inventory/index record for a plasmid-like material.""" + + identity: str + display_id: str | None = None + name: str | None = None + state: MaterialState = MaterialState.PLANNED + roles: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None + + +@dataclass +class IndexedBackbone: + """Inventory/index record for a backbone material.""" + + identity: str + display_id: str | None = None + name: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None diff --git a/src/buildcompiler/domain/reagent.py b/src/buildcompiler/domain/reagent.py new file mode 100644 index 0000000..b50e690 --- /dev/null +++ b/src/buildcompiler/domain/reagent.py @@ -0,0 +1,15 @@ +"""Normalized reagent record contracts.""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class IndexedReagent: + """Inventory/index record for reagents.""" + + identity: str + display_id: str | None = None + name: str | None = None + reagent_type: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/status.py b/src/buildcompiler/domain/status.py new file mode 100644 index 0000000..aac2ba3 --- /dev/null +++ b/src/buildcompiler/domain/status.py @@ -0,0 +1,25 @@ +"""Status enums and contract-level semantics helpers.""" + +from enum import Enum + + +class StageStatus(str, Enum): + """Status for a single stage result. + + BLOCKED means expected inputs or approvals can unblock this stage later. + FAILED means the request cannot proceed without changing design/options/ + collections/approval state. + """ + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + BLOCKED = "blocked" + FAILED = "failed" + + +class BuildStatus(str, Enum): + """Status for full-build aggregate results.""" + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + FAILED = "failed" diff --git a/src/buildcompiler/domain/warnings.py b/src/buildcompiler/domain/warnings.py new file mode 100644 index 0000000..72f130d --- /dev/null +++ b/src/buildcompiler/domain/warnings.py @@ -0,0 +1,17 @@ +"""Domain warning contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage + + +@dataclass +class BuildWarning: + """Structured non-fatal warning for planning/execution/reporting.""" + + code: str + message: str + stage: BuildStage | None = None + source_identity: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/execution/__init__.py b/src/buildcompiler/execution/__init__.py new file mode 100644 index 0000000..a8357fa --- /dev/null +++ b/src/buildcompiler/execution/__init__.py @@ -0,0 +1,6 @@ +"""Execution package exports.""" + +from .context import BuildContext +from .executor import FullBuildExecutor + +__all__ = ["BuildContext", "FullBuildExecutor"] diff --git a/src/buildcompiler/execution/context.py b/src/buildcompiler/execution/context.py new file mode 100644 index 0000000..9c17cdf --- /dev/null +++ b/src/buildcompiler/execution/context.py @@ -0,0 +1,20 @@ +"""Execution context contract for full-build orchestration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +@dataclass +class BuildContext: + sbol: SbolResolver + inventory: Inventory + build_document: Any + options: Any + adapters: Any = None + graph: Any = None + logger: Any = None diff --git a/src/buildcompiler/execution/executor.py b/src/buildcompiler/execution/executor.py new file mode 100644 index 0000000..4a1a064 --- /dev/null +++ b/src/buildcompiler/execution/executor.py @@ -0,0 +1,310 @@ +"""Bounded fixed-point full-build executor.""" + +from __future__ import annotations + +import hashlib +from collections import OrderedDict +from typing import Any + +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + FullBuildResult, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution.context import BuildContext +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver +from buildcompiler.stages import ( + AssemblyLvl1Stage, + AssemblyLvl2Stage, + DomesticationStage, +) + + +class FullBuildExecutor: + def __init__( + self, + *, + context: BuildContext, + lvl2_stage: Any | None = None, + lvl1_stage: Any | None = None, + domestication_stage: Any | None = None, + transformation_stage: Any | None = None, + plating_stage: Any | None = None, + ) -> None: + self.context = context + options = context.options + self.lvl2_stage = lvl2_stage or AssemblyLvl2Stage( + inventory=context.inventory, options=options + ) + self.lvl1_stage = lvl1_stage or AssemblyLvl1Stage( + inventory=context.inventory, options=options + ) + self.domestication_stage = domestication_stage or DomesticationStage( + inventory=context.inventory, options=options + ) + self.transformation_stage = transformation_stage + self.plating_stage = plating_stage + + @classmethod + def from_dependencies( + cls, + *, + inventory: Inventory, + sbol_document: Any, + options: BuildOptions, + adapters: Any = None, + graph: Any = None, + logger: Any = None, + **stage_overrides: Any, + ) -> "FullBuildExecutor": + resolver = SbolResolver(sbol_document) + return cls( + context=BuildContext( + sbol=resolver, + inventory=inventory, + build_document=sbol_document, + options=options, + adapters=adapters, + graph=graph, + logger=logger, + ), + **stage_overrides, + ) + + def execute( + self, plan: BuildPlan, *, options: BuildOptions | None = None + ) -> FullBuildResult: + if options is not None: + self.context.options = options + + pending = { + BuildStage.ASSEMBLY_LVL2: OrderedDict( + (r.id, r) for r in plan.lvl2_requests + ), + BuildStage.ASSEMBLY_LVL1: OrderedDict( + (r.id, r) for r in plan.lvl1_requests + ), + BuildStage.DOMESTICATION: OrderedDict( + (r.id, r) for r in plan.domestication_requests + ), + } + completed: set[str] = set() + stage_results: list[StageResult] = [] + final_products: dict[str, Any] = {} + missing_by_key: dict[tuple, MissingBuildInput] = {} + approvals: dict[str, Any] = {} + warnings: list[Any] = list(plan.warnings) + seen_products: set[str] = set() + transformed: set[str] = set() + plated: set[str] = set() + + for _ in range(self.context.options.execution.max_iterations): + progress = False + for stage in ( + BuildStage.ASSEMBLY_LVL2, + BuildStage.ASSEMBLY_LVL1, + BuildStage.DOMESTICATION, + ): + runner = { + BuildStage.ASSEMBLY_LVL2: self.lvl2_stage, + BuildStage.ASSEMBLY_LVL1: self.lvl1_stage, + BuildStage.DOMESTICATION: self.domestication_stage, + }[stage] + for request in list(pending[stage].values()): + result = self._run_stage(runner, request) + stage_results.append(result) + warnings.extend(result.warnings) + for approval in result.required_approvals: + approvals[str(approval)] = approval + if result.status in ( + StageStatus.SUCCESS, + StageStatus.PARTIAL_SUCCESS, + ): + if request.id not in completed: + completed.add(request.id) + progress = True + pending[stage].pop(request.id, None) + progress = ( + self._index_products(result, final_products, seen_products) + or progress + ) + progress = ( + self._chain( + result.products, stage_results, transformed, plated + ) + or progress + ) + else: + for missing in result.missing_inputs: + missing_by_key[self._missing_key(missing)] = missing + promoted = self._promote(request, missing) + if ( + promoted is not None + and promoted.id not in pending[promoted.stage] + and promoted.id not in completed + ): + pending[promoted.stage][promoted.id] = promoted + progress = True + + if not any(pending[s] for s in pending): + break + if not progress: + break + + unresolved = [ + m + for m in missing_by_key.values() + if self._promote(None, m) is None + or self._promote(None, m).id not in completed + ] + products = list(final_products.values()) + status = ( + BuildStatus.SUCCESS + if (not unresolved and not any(pending[s] for s in pending)) + else (BuildStatus.PARTIAL_SUCCESS if products else BuildStatus.FAILED) + ) + from buildcompiler.reporting import build_graph, build_report, build_summary + + preliminary_result = FullBuildResult( + status=status, + plan=plan, + build_document=self.context.build_document, + stage_results=stage_results, + graph=None, + final_products=products, + missing_inputs=unresolved, + required_approvals=list(approvals.values()), + warnings=warnings, + summary=None, + report=None, + ) + graph = build_graph(preliminary_result) + report = ( + build_report(preliminary_result, graph=graph) + if self.context.options.reporting.include_detailed_report + else None + ) + final_result = FullBuildResult( + status=status, + plan=plan, + build_document=self.context.build_document, + stage_results=stage_results, + graph=graph, + final_products=products, + missing_inputs=unresolved, + required_approvals=list(approvals.values()), + warnings=warnings, + summary=None, + report=report, + ) + final_result.summary = build_summary(final_result) + return final_result + + def _run_stage(self, stage: Any, request: BuildRequest) -> StageResult: + source_document = ( + getattr(self.context.sbol, "document", None) or self.context.build_document + ) + try: + return stage.run( + request, + source_document=source_document, + target_document=self.context.build_document, + ) + except Exception: + if not self.context.options.execution.continue_on_error: + raise + return StageResult( + id=f"{request.id}:{request.stage.value}", + stage=request.stage, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=["Unexpected execution error."], + ) + + def _index_products( + self, + result: StageResult, + final_products: dict[str, Any], + seen_products: set[str], + ) -> bool: + progress = False + for product in result.products: + if product.identity not in seen_products: + seen_products.add(product.identity) + self.context.inventory.add_generated_product(product) + final_products[product.identity] = product + progress = True + return progress + + def _promote( + self, request: BuildRequest | None, missing: MissingBuildInput + ) -> BuildRequest | None: + if missing.required_stage not in ( + BuildStage.ASSEMBLY_LVL1, + BuildStage.DOMESTICATION, + ): + return None + prefix = ( + "lvl1" + if missing.required_stage == BuildStage.ASSEMBLY_LVL1 + else "domestication" + ) + digest = hashlib.sha1(missing.missing_identity.encode()).hexdigest()[:12] + return BuildRequest( + id=f"promoted:{prefix}:{digest}", + stage=missing.required_stage, + source_identity=missing.missing_identity, + source_display_id=missing.missing_display_id, + source_kind=DesignKind.COMPONENT_DEFINITION, + parent_group=request.id if request else missing.source_design_identity, + constraints={ + "promoted_from_stage": missing.source_stage.value, + "promoted_from_design_identity": missing.source_design_identity, + "candidates_tried": list(missing.candidates_tried), + }, + ) + + def _missing_key(self, missing: MissingBuildInput) -> tuple: + return ( + missing.source_stage.value, + missing.source_design_identity, + missing.missing_identity, + missing.missing_kind, + str(missing.required_stage), + missing.reason, + ) + + def _chain( + self, + products: list[Any], + stage_results: list[StageResult], + transformed: set[str], + plated: set[str], + ) -> bool: + progress = False + if self.transformation_stage is None: + return False + for product in products: + if product.identity in transformed: + continue + transformed.add(product.identity) + t_result = self.transformation_stage.run(product) + stage_results.append(t_result) + progress = True + if self.plating_stage is None: + continue + for out in t_result.products: + if out.identity in plated: + continue + plated.add(out.identity) + stage_results.append(self.plating_stage.run(out)) + progress = True + return progress diff --git a/src/buildcompiler/inventory/__init__.py b/src/buildcompiler/inventory/__init__.py new file mode 100644 index 0000000..403b6e6 --- /dev/null +++ b/src/buildcompiler/inventory/__init__.py @@ -0,0 +1,14 @@ +"""Inventory package exports for deterministic lookup/indexing contracts.""" + +from .compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection +from .inventory import Inventory +from .selector import CompatibilitySelector + +__all__ = [ + "CompatibilitySelector", + "Inventory", + "Lvl1Route", + "Lvl2Route", + "RouteScore", + "RouteSelection", +] diff --git a/src/buildcompiler/inventory/compatibility.py b/src/buildcompiler/inventory/compatibility.py new file mode 100644 index 0000000..52ddcb8 --- /dev/null +++ b/src/buildcompiler/inventory/compatibility.py @@ -0,0 +1,58 @@ +"""Deterministic compatibility route models and score ordering.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid + + +@dataclass(frozen=True) +class RouteScore: + missing_required_products: int = 0 + missing_domestications: int = 0 + missing_lvl1_plasmids: int = 0 + generated_or_planned_materials: int = 0 + lower_material_state_penalty: int = 0 + constraint_violations: int = 0 + total_assemblies: int = 0 + identity_tiebreak: tuple[str, ...] = () + + def sort_key(self) -> tuple[int, int, int, int, int, int, int, tuple[str, ...]]: + """Lower sort key is a better route.""" + return ( + self.constraint_violations, + self.missing_required_products, + self.missing_domestications, + self.missing_lvl1_plasmids, + self.generated_or_planned_materials, + self.lower_material_state_penalty, + self.total_assemblies, + self.identity_tiebreak, + ) + + +@dataclass(frozen=True) +class Lvl1Route: + request_id: str + part_identities: tuple[str, ...] + selected_part_plasmids: tuple[IndexedPlasmid, ...] + missing_part_identities: tuple[str, ...] + backbone: IndexedBackbone | None + score: RouteScore + + +@dataclass(frozen=True) +class Lvl2Route: + request_id: str + region_order: tuple[str, ...] + selected_lvl1_plasmids: tuple[IndexedPlasmid, ...] + missing_region_identities: tuple[str, ...] + backbone: IndexedBackbone | None + score: RouteScore + + +@dataclass(frozen=True) +class RouteSelection: + selected: Lvl1Route | Lvl2Route | None + rejected: tuple[Lvl1Route | Lvl2Route, ...] = () diff --git a/src/buildcompiler/inventory/inventory.py b/src/buildcompiler/inventory/inventory.py new file mode 100644 index 0000000..fb06999 --- /dev/null +++ b/src/buildcompiler/inventory/inventory.py @@ -0,0 +1,202 @@ +"""Normalized inventory facade with eager deterministic indexes.""" + +from __future__ import annotations + +from collections import defaultdict + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) + + +_MATERIAL_ORDER = { + MaterialState.PLANNED: 0, + MaterialState.GENERATED: 1, + MaterialState.ASSEMBLED: 2, + MaterialState.TRANSFORMED: 3, + MaterialState.PLATED: 4, +} + + +class Inventory: + def __init__( + self, + *, + plasmids: list[IndexedPlasmid] | None = None, + backbones: list[IndexedBackbone] | None = None, + reagents: list[IndexedReagent] | None = None, + ) -> None: + self.plasmids_by_identity: dict[str, IndexedPlasmid] = {} + self.plasmids_by_insert_identity: dict[str, list[IndexedPlasmid]] = defaultdict(list) + self.plasmids_by_fusion_sites: dict[tuple[str, ...], list[IndexedPlasmid]] = defaultdict(list) + self.plasmids_by_antibiotic: dict[str, list[IndexedPlasmid]] = defaultdict(list) + + self.backbones_by_identity: dict[str, IndexedBackbone] = {} + self.backbones_by_fusion_sites_and_antibiotic: dict[ + tuple[tuple[str, ...], str], list[IndexedBackbone] + ] = defaultdict(list) + + self.reagents_by_identity: dict[str, IndexedReagent] = {} + self.reagents_by_name: dict[str, IndexedReagent] = {} + + self.generated_products_by_identity: dict[str, IndexedPlasmid] = {} + + for plasmid in plasmids or []: + self._add_plasmid(plasmid) + for backbone in backbones or []: + self._add_backbone(backbone) + for reagent in reagents or []: + self._add_reagent(reagent) + + def _sorted_plasmids(self, items: list[IndexedPlasmid]) -> list[IndexedPlasmid]: + return sorted(items, key=lambda p: p.identity) + + def _backbone_stage(self, backbone: IndexedBackbone) -> BuildStage | None: + raw = backbone.metadata.get("stage") if backbone.metadata else None + if raw is None: + return None + if isinstance(raw, BuildStage): + return raw + try: + return BuildStage(raw) + except ValueError: + return None + + def _remove_plasmid_from_secondary_indexes(self, plasmid: IndexedPlasmid) -> None: + for insert_identity in sorted(plasmid.metadata.get("insert_identities", [])): + existing = self.plasmids_by_insert_identity.get(insert_identity, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_insert_identity[insert_identity] = filtered + else: + self.plasmids_by_insert_identity.pop(insert_identity, None) + + fusion_sites = tuple(plasmid.metadata.get("fusion_sites", ())) + if fusion_sites: + existing = self.plasmids_by_fusion_sites.get(fusion_sites, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_fusion_sites[fusion_sites] = filtered + else: + self.plasmids_by_fusion_sites.pop(fusion_sites, None) + + antibiotic = plasmid.metadata.get("antibiotic") + if antibiotic: + existing = self.plasmids_by_antibiotic.get(antibiotic, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_antibiotic[antibiotic] = filtered + else: + self.plasmids_by_antibiotic.pop(antibiotic, None) + + def _add_plasmid(self, plasmid: IndexedPlasmid) -> None: + existing = self.plasmids_by_identity.get(plasmid.identity) + if existing is not None: + self._remove_plasmid_from_secondary_indexes(existing) + + self.plasmids_by_identity[plasmid.identity] = plasmid + for insert_identity in sorted(plasmid.metadata.get("insert_identities", [])): + self.plasmids_by_insert_identity[insert_identity].append(plasmid) + self.plasmids_by_insert_identity[insert_identity] = self._sorted_plasmids( + self.plasmids_by_insert_identity[insert_identity] + ) + + fusion_sites = tuple(plasmid.metadata.get("fusion_sites", ())) + if fusion_sites: + self.plasmids_by_fusion_sites[fusion_sites].append(plasmid) + self.plasmids_by_fusion_sites[fusion_sites] = self._sorted_plasmids( + self.plasmids_by_fusion_sites[fusion_sites] + ) + + antibiotic = plasmid.metadata.get("antibiotic") + if antibiotic: + self.plasmids_by_antibiotic[antibiotic].append(plasmid) + self.plasmids_by_antibiotic[antibiotic] = self._sorted_plasmids( + self.plasmids_by_antibiotic[antibiotic] + ) + + def _add_backbone(self, backbone: IndexedBackbone) -> None: + self.backbones_by_identity[backbone.identity] = backbone + fusion_sites = tuple(backbone.metadata.get("fusion_sites", ())) + antibiotic = backbone.metadata.get("antibiotic") + if fusion_sites and antibiotic: + key = (fusion_sites, antibiotic) + self.backbones_by_fusion_sites_and_antibiotic[key].append(backbone) + self.backbones_by_fusion_sites_and_antibiotic[key] = sorted( + self.backbones_by_fusion_sites_and_antibiotic[key], + key=lambda b: b.identity, + ) + + def _add_reagent(self, reagent: IndexedReagent) -> None: + self.reagents_by_identity[reagent.identity] = reagent + if reagent.name: + self.reagents_by_name[reagent.name] = reagent + + def find_single_part_plasmids( + self, part_identity: str, *, antibiotic: str | None = None + ) -> list[IndexedPlasmid]: + matches = list(self.plasmids_by_insert_identity.get(part_identity, [])) + if antibiotic is not None: + matches = [p for p in matches if p.metadata.get("antibiotic") == antibiotic] + return self._sorted_plasmids(matches) + + def find_lvl1_region_plasmids( + self, + region_identity: str, + *, + min_material_state: MaterialState = MaterialState.PLANNED, + ) -> list[IndexedPlasmid]: + matches = self.plasmids_by_insert_identity.get(region_identity, []) + min_rank = _MATERIAL_ORDER[min_material_state] + filtered = [p for p in matches if _MATERIAL_ORDER[p.state] >= min_rank] + return self._sorted_plasmids(filtered) + + def find_backbone( + self, + *, + fusion_sites: tuple[str, ...] | None = None, + antibiotic: str | None = None, + stage: BuildStage | None = None, + ) -> IndexedBackbone | None: + if fusion_sites is not None and antibiotic is not None: + candidates = list( + self.backbones_by_fusion_sites_and_antibiotic.get( + (tuple(fusion_sites), antibiotic), [] + ) + ) + else: + candidates = sorted(self.backbones_by_identity.values(), key=lambda b: b.identity) + if fusion_sites is not None: + candidates = [ + b for b in candidates if tuple(b.metadata.get("fusion_sites", ())) == tuple(fusion_sites) + ] + if antibiotic is not None: + candidates = [b for b in candidates if b.metadata.get("antibiotic") == antibiotic] + if stage is not None: + candidates = [b for b in candidates if self._backbone_stage(b) == stage] + return candidates[0] if candidates else None + + def find_restriction_enzyme(self, name: str) -> IndexedReagent | None: + reagent = self.reagents_by_name.get(name) + if reagent and reagent.reagent_type == "restriction_enzyme": + return reagent + return None + + def find_ligase(self, preferred: str | None = None) -> IndexedReagent | None: + if preferred: + reagent = self.reagents_by_name.get(preferred) + if reagent and reagent.reagent_type == "ligase": + return reagent + ligases = sorted( + (r for r in self.reagents_by_identity.values() if r.reagent_type == "ligase"), + key=lambda r: r.identity, + ) + return ligases[0] if ligases else None + + def add_generated_product(self, product: IndexedPlasmid) -> None: + self.generated_products_by_identity[product.identity] = product + self._add_plasmid(product) diff --git a/src/buildcompiler/inventory/selector.py b/src/buildcompiler/inventory/selector.py new file mode 100644 index 0000000..1b712e9 --- /dev/null +++ b/src/buildcompiler/inventory/selector.py @@ -0,0 +1,165 @@ +"""Deterministic compatibility selector for lvl1/lvl2 route selection.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from itertools import permutations +from typing import Any + +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import BuildStage, MaterialState +from buildcompiler.inventory.compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection +from buildcompiler.inventory.inventory import Inventory + + +_STATE_RANK = { + MaterialState.PLANNED: 0, + MaterialState.GENERATED: 1, + MaterialState.ASSEMBLED: 2, + MaterialState.TRANSFORMED: 3, + MaterialState.PLATED: 4, +} + + +class CompatibilitySelector: + def __init__(self, inventory: Inventory, *, options: BuildOptions | None = None) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + + def _is_generated_or_planned(self, plasmid: Any) -> bool: + source = (plasmid.metadata or {}).get("source", "") + if source: + return source in {"generated", "planned"} + return plasmid.state in {MaterialState.PLANNED, MaterialState.GENERATED} + + def _constraint_filter(self, items: list[Any], constraints: Mapping[str, Any]) -> list[Any]: + allowed = set(constraints.get("allowed_identities", [])) + forbidden = set(constraints.get("forbidden_identities", [])) + antibiotic = constraints.get("antibiotic") + out = [] + for item in items: + if allowed and item.identity not in allowed: + continue + if item.identity in forbidden: + continue + if antibiotic and item.metadata.get("antibiotic") != antibiotic: + continue + out.append(item) + return out + + def _best_candidate(self, candidates: list[Any], constraints: Mapping[str, Any]) -> Any | None: + filtered = self._constraint_filter(candidates, constraints) + if not filtered: + return None + + prefer_existing = self.options.selection.prefer_existing_collection_material + prefer_state = self.options.selection.prefer_higher_material_state + + def _key(p: Any) -> tuple[int, int, str]: + generated_penalty = int(prefer_existing and self._is_generated_or_planned(p)) + state_penalty = -_STATE_RANK[p.state] if prefer_state else 0 + return (generated_penalty, state_penalty, p.identity) + + return sorted(filtered, key=_key)[0] + + def select_lvl1_route(self, *, request_id: str, part_identities: Sequence[str], constraints: Mapping[str, Any] | None = None) -> RouteSelection: + active_constraints = constraints or {} + selected = [] + missing = [] + for part_identity in part_identities: + candidates = self.inventory.find_single_part_plasmids(part_identity, antibiotic=active_constraints.get("antibiotic")) + choice = self._best_candidate(candidates, active_constraints) + if choice is None: + missing.append(part_identity) + else: + selected.append(choice) + + backbone = self.inventory.find_backbone( + fusion_sites=tuple(active_constraints["fusion_sites"]) if "fusion_sites" in active_constraints else None, + antibiotic=active_constraints.get("antibiotic"), + stage=BuildStage.ASSEMBLY_LVL1, + ) + score = RouteScore( + missing_required_products=len(missing), + missing_domestications=len(missing), + generated_or_planned_materials=sum(1 for p in selected if self._is_generated_or_planned(p)), + lower_material_state_penalty=sum((_STATE_RANK[MaterialState.PLATED] - _STATE_RANK[p.state]) for p in selected) + if self.options.selection.prefer_higher_material_state + else 0, + identity_tiebreak=tuple(sorted(p.identity for p in selected)) + tuple(missing), + ) + route = Lvl1Route(request_id, tuple(part_identities), tuple(selected), tuple(missing), backbone, score) + return RouteSelection(selected=route, rejected=()) + + def select_lvl2_route(self, *, request_id: str, region_identities: Sequence[str], constraints: Mapping[str, Any] | None = None) -> RouteSelection: + active_constraints = constraints or {} + max_regions = self.options.planning.lvl2_search.max_exhaustive_region_count + allow_large = self.options.planning.lvl2_search.allow_large_order_search + + if "region_order" in active_constraints: + constrained_order = tuple(active_constraints["region_order"]) + requested_regions = tuple(region_identities) + if sorted(constrained_order) != sorted(requested_regions): + blocked = Lvl2Route( + request_id=request_id, + region_order=constrained_order, + selected_lvl1_plasmids=(), + missing_region_identities=requested_regions, + backbone=None, + score=RouteScore( + missing_required_products=len(requested_regions), + missing_lvl1_plasmids=len(requested_regions), + constraint_violations=1, + identity_tiebreak=requested_regions, + ), + ) + return RouteSelection(selected=None, rejected=(blocked,)) + orders = [constrained_order] + elif len(region_identities) > max_regions and not allow_large: + blocked = Lvl2Route( + request_id=request_id, + region_order=tuple(region_identities), + selected_lvl1_plasmids=(), + missing_region_identities=tuple(region_identities), + backbone=None, + score=RouteScore( + missing_required_products=len(region_identities), + missing_lvl1_plasmids=len(region_identities), + constraint_violations=1, + identity_tiebreak=tuple(region_identities), + ), + ) + return RouteSelection(selected=None, rejected=(blocked,)) + else: + orders = sorted(set(permutations(region_identities))) + + routes = [] + for order in orders: + selected = [] + missing = [] + for region in order: + candidates = self.inventory.find_lvl1_region_plasmids(region) + choice = self._best_candidate(candidates, active_constraints) + if choice is None: + missing.append(region) + else: + selected.append(choice) + score = RouteScore( + missing_required_products=len(missing), + missing_lvl1_plasmids=len(missing), + generated_or_planned_materials=sum(1 for p in selected if self._is_generated_or_planned(p)), + lower_material_state_penalty=sum((_STATE_RANK[MaterialState.PLATED] - _STATE_RANK[p.state]) for p in selected) + if self.options.selection.prefer_higher_material_state + else 0, + total_assemblies=int(bool(missing)), + identity_tiebreak=tuple(p.identity for p in selected) + tuple(missing), + ) + backbone = self.inventory.find_backbone( + fusion_sites=tuple(active_constraints["fusion_sites"]) if "fusion_sites" in active_constraints else None, + antibiotic=active_constraints.get("antibiotic"), + stage=BuildStage.ASSEMBLY_LVL2, + ) + routes.append(Lvl2Route(request_id, tuple(order), tuple(selected), tuple(missing), backbone, score)) + + ranked = sorted(routes, key=lambda r: r.score.sort_key()) + return RouteSelection(selected=ranked[0] if ranked else None, rejected=tuple(ranked[1:4])) diff --git a/src/buildcompiler/planning/__init__.py b/src/buildcompiler/planning/__init__.py new file mode 100644 index 0000000..483aae4 --- /dev/null +++ b/src/buildcompiler/planning/__init__.py @@ -0,0 +1,14 @@ +"""Planning package exports.""" + +from .domestication import DomesticationPlan, DomesticationPlanner, SequenceEditProposal +from .full_build_planner import FullBuildPlanner +from .models import BuildPlan, UnsupportedPlanningRecord + +__all__ = [ + "BuildPlan", + "UnsupportedPlanningRecord", + "FullBuildPlanner", + "DomesticationPlan", + "DomesticationPlanner", + "SequenceEditProposal", +] diff --git a/src/buildcompiler/planning/classifier.py b/src/buildcompiler/planning/classifier.py new file mode 100644 index 0000000..5d666ae --- /dev/null +++ b/src/buildcompiler/planning/classifier.py @@ -0,0 +1,112 @@ +"""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 + +RECOMMENDED_LVL1_PARTS = ("promoter", "rbs", "cds", "terminator") + + +def _stable_slug(identity: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", identity.lower()).strip("-")[-48:] + + +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 + + +def classify_non_combinatorial( + design: object, +) -> BuildRequest | UnsupportedPlanningRecord: + if isinstance(design, sbol2.ModuleDefinition): + return BuildRequest( + request_id_for(BuildStage.ASSEMBLY_LVL2, design.identity, design.displayId), + BuildStage.ASSEMBLY_LVL2, + design.identity, + design.displayId, + DesignKind.MODULE_DEFINITION, + ) + + 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__}", + ) diff --git a/src/buildcompiler/planning/combinatorial.py b/src/buildcompiler/planning/combinatorial.py new file mode 100644 index 0000000..101ec02 --- /dev/null +++ b/src/buildcompiler/planning/combinatorial.py @@ -0,0 +1,113 @@ +from __future__ import annotations +import itertools +import sbol2 +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import BuildRequest, BuildStage, BuildWarning, DesignKind +from buildcompiler.planning.classifier import request_id_for +from buildcompiler.planning.models import UnsupportedPlanningRecord +from buildcompiler.planning.validation import ROLE_TO_NAME + + +def _collect_variant_sets(derivation): + variables = list(derivation.variableComponents) + variables.sort( + key=lambda variable: (str(getattr(variable, "variable", "")), variable.identity) + ) + return variables, [sorted(list(vc.variants), key=str) for vc in variables] + + +def expand_combinatorial_derivation( + derivation: sbol2.CombinatorialDerivation, *, options: BuildOptions +): + warnings = [] + unsupported = [] + requests = [] + variables, variant_sets = _collect_variant_sets(derivation) + if not variant_sets or any(len(v) == 0 for v in variant_sets): + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "Variable component has no listed variants.", + ) + ) + return requests, unsupported, warnings + total = 1 + for v in variant_sets: + total *= len(v) + if ( + total > options.planning.combinatorial.max_variants + and not options.planning.combinatorial.allow_large_expansion + ): + warnings.append( + BuildWarning( + "planning.combinatorial.expansion_blocked", + "Combinatorial expansion exceeds max_variants and is blocked.", + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + { + "variant_count": total, + "max_variants": options.planning.combinatorial.max_variants, + }, + ) + ) + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "Combinatorial expansion blocked by max_variants.", + {"variant_count": total}, + ) + ) + return requests, unsupported, warnings + doc = derivation.doc + valid = 0 + for idx, chosen in enumerate(itertools.product(*variant_sets)): + roles = [] + for pid in chosen: + obj = doc.find(pid) if doc is not None else None + if isinstance(obj, sbol2.ComponentDefinition): + matching = [ROLE_TO_NAME[r] for r in obj.roles if r in ROLE_TO_NAME] + if len(matching) == 1: + roles.append(matching[0]) + if sorted(roles) != ["cds", "promoter", "rbs", "terminator"]: + warnings.append( + BuildWarning( + "planning.combinatorial.invalid_variant", + "Skipping invalid combinatorial variant.", + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + {"variant_index": idx}, + ) + ) + continue + requests.append( + BuildRequest( + request_id_for( + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + derivation.displayId, + variant_index=idx, + ), + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + parent_group=derivation.identity, + variant_index=idx, + constraints={"part_order": list(chosen)}, + ) + ) + valid += 1 + if valid == 0: + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "All combinatorial variants were invalid.", + ) + ) + return requests, unsupported, warnings diff --git a/src/buildcompiler/planning/domestication.py b/src/buildcompiler/planning/domestication.py new file mode 100644 index 0000000..00aea97 --- /dev/null +++ b/src/buildcompiler/planning/domestication.py @@ -0,0 +1,83 @@ +"""Deterministic domestication planning helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import sbol2 + +from buildcompiler.domain import BuildStage, BuildWarning +from buildcompiler.planning.validation import classify_part_role + + +@dataclass +class SequenceEditProposal: + source_identity: str + enzyme_name: str + site_sequence: str + position: int + original_sequence: str + proposed_sequence: str + reason: str + approved: bool = False + + +@dataclass +class DomesticationPlan: + part_identity: str + part_display_id: str | None + part_role: str + backbone_identity: str | None = None + restriction_enzyme_name: str = "BsaI" + ligase_name: str = "T4_DNA_ligase" + sequence_edit_proposals: list[SequenceEditProposal] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + + +class DomesticationPlanner: + """Pure planner for domestication requirements for a single part.""" + + _BSAI_SITES = ("GGTCTC", "GAGACC") + + def plan(self, part_component: sbol2.ComponentDefinition) -> DomesticationPlan: + part_role = classify_part_role(part_component) + if part_role is None: + raise ValueError( + f"Unsupported domestication role for part {part_component.identity}; expected promoter/rbs/cds/terminator" + ) + + sequence = self._resolve_sequence(part_component) + proposals: list[SequenceEditProposal] = [] + for site in self._BSAI_SITES: + start = 0 + while True: + index = sequence.find(site, start) + if index < 0: + break + proposals.append( + SequenceEditProposal( + source_identity=part_component.identity, + enzyme_name="BsaI", + site_sequence=site, + position=index, + original_sequence=site, + proposed_sequence=f"{site[:-1]}A", + reason="Internal BsaI recognition site detected; human-reviewed edit required.", + ) + ) + start = index + 1 + + return DomesticationPlan( + part_identity=part_component.identity, + part_display_id=part_component.displayId, + part_role=part_role, + sequence_edit_proposals=proposals, + ) + + def _resolve_sequence(self, part_component: sbol2.ComponentDefinition) -> str: + for sequence_ref in part_component.sequences: + sequence_obj = part_component.doc.find(sequence_ref) if part_component.doc else None + elements = getattr(sequence_obj, "elements", None) + if isinstance(elements, str) and elements: + return elements.upper() + raise ValueError(f"Part {part_component.identity} is missing a usable DNA sequence") diff --git a/src/buildcompiler/planning/full_build_planner.py b/src/buildcompiler/planning/full_build_planner.py new file mode 100644 index 0000000..8814fbf --- /dev/null +++ b/src/buildcompiler/planning/full_build_planner.py @@ -0,0 +1,49 @@ +"""Pure full-build planner orchestration.""" + +from __future__ import annotations + +from collections.abc import Iterable + +import sbol2 + +from buildcompiler.api.options import BuildOptions +from buildcompiler.planning.classifier import classify_non_combinatorial +from buildcompiler.planning.combinatorial import expand_combinatorial_derivation +from buildcompiler.planning.models import BuildPlan, UnsupportedPlanningRecord +from buildcompiler.sbol import SbolResolver + + +class FullBuildPlanner: + def __init__( + self, + *, + options: BuildOptions | None = None, + resolver: SbolResolver | None = None, + ): + self.options = options or BuildOptions() + self.resolver = resolver + + def plan( + self, abstract_designs: Iterable[object], *, options: BuildOptions | None = None + ) -> BuildPlan: + active = options or self.options + out = BuildPlan() + for design in abstract_designs: + if isinstance(design, sbol2.CombinatorialDerivation): + reqs, unsupported, warnings = expand_combinatorial_derivation( + design, options=active + ) + out.lvl1_requests.extend(reqs) + out.unsupported.extend(unsupported) + out.warnings.extend(warnings) + continue + classified = classify_non_combinatorial(design) + if isinstance(classified, UnsupportedPlanningRecord): + out.unsupported.append(classified) + elif classified.stage.value == "assembly_lvl2": + out.lvl2_requests.append(classified) + elif classified.stage.value == "assembly_lvl1": + out.lvl1_requests.append(classified) + else: + out.domestication_requests.append(classified) + return out diff --git a/src/buildcompiler/planning/models.py b/src/buildcompiler/planning/models.py new file mode 100644 index 0000000..d4782c7 --- /dev/null +++ b/src/buildcompiler/planning/models.py @@ -0,0 +1,24 @@ +"""Planning contracts for full-build planning output.""" + +from dataclasses import dataclass, field +from typing import Any + +from buildcompiler.domain import BuildRequest, BuildWarning, DesignKind + + +@dataclass +class UnsupportedPlanningRecord: + source_identity: str + source_display_id: str | None + source_kind: DesignKind + reason: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildPlan: + lvl2_requests: list[BuildRequest] = field(default_factory=list) + lvl1_requests: list[BuildRequest] = field(default_factory=list) + domestication_requests: list[BuildRequest] = field(default_factory=list) + unsupported: list[UnsupportedPlanningRecord] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) diff --git a/src/buildcompiler/planning/validation.py b/src/buildcompiler/planning/validation.py new file mode 100644 index 0000000..b00c70e --- /dev/null +++ b/src/buildcompiler/planning/validation.py @@ -0,0 +1,140 @@ +"""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"] + + +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] + + +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 + + +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 diff --git a/src/buildcompiler/plasmid.py b/src/buildcompiler/plasmid.py index 9b81dfe..3907cc1 100644 --- a/src/buildcompiler/plasmid.py +++ b/src/buildcompiler/plasmid.py @@ -12,8 +12,8 @@ def __init__( self, definition: sbol2.ComponentDefinition, strain_definition: sbol2.ModuleDefinition, - plasmid_implementations: sbol2.Implementation, - strain_implementations: sbol2.Implementation, + plasmid_implementations: List[sbol2.Implementation], + strain_implementations: List[sbol2.Implementation], doc: sbol2.document, ): self.plasmid_definition = definition @@ -35,8 +35,7 @@ def _match_fusion_sites(self, doc: sbol2.document) -> List[str]: if seq == sequence.upper(): fusion_sites.append(key) - fusion_sites.sort() - return fusion_sites + return [fusion_sites[0], fusion_sites[-1]] def _get_antibiotic_resistance(self, doc: sbol2.Document) -> str: for component in ( @@ -94,26 +93,3 @@ def __eq__(self, other): def __hash__(self): return hash(self.plasmid_definition) - - -# def _extract_fusion_sites( -# plasmid: sbol2.ComponentDefinition, -# doc: sbol2.Document, -# sbh: sbol2.PartShop -# ) -> List[sbol2.ComponentDefinition]: -# """ -# Returns all fusion site component definitions from a plasmid. - -# Args: -# plasmid: :class:`sbol2.ComponentDefinition` representing the plasmid. - -# Returns: -# A list of fusion site component definitions. -# """ -# fusion_sites = [] -# for component in plasmid.components: -# definition = get_or_pull(doc, sbh, component.definition) -# if RESTRICTION_ENZYME_ASSEMBLY_SCAR in definition.roles: -# fusion_sites.append(definition) - -# return fusion_sites diff --git a/src/buildcompiler/reporting/__init__.py b/src/buildcompiler/reporting/__init__.py new file mode 100644 index 0000000..f69bcdc --- /dev/null +++ b/src/buildcompiler/reporting/__init__.py @@ -0,0 +1,27 @@ +"""Reporting models and builders.""" + +from .graph import BuildGraph, BuildGraphEdge, BuildGraphNode, build_graph +from .report import ( + BuildReport, + DependencyChainStep, + RecommendedAction, + RouteReport, + StageReportSection, + build_report, +) +from .summary import BuildSummary, build_summary + +__all__ = [ + "BuildGraph", + "BuildGraphEdge", + "BuildGraphNode", + "BuildReport", + "BuildSummary", + "DependencyChainStep", + "RecommendedAction", + "RouteReport", + "StageReportSection", + "build_graph", + "build_report", + "build_summary", +] diff --git a/src/buildcompiler/reporting/graph.py b/src/buildcompiler/reporting/graph.py new file mode 100644 index 0000000..f9eba65 --- /dev/null +++ b/src/buildcompiler/reporting/graph.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from buildcompiler.domain import FullBuildResult + + +@dataclass(frozen=True) +class BuildGraphNode: + id: str + kind: str + label: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class BuildGraphEdge: + source: str + target: str + relationship: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildGraph: + nodes: list[BuildGraphNode] = field(default_factory=list) + edges: list[BuildGraphEdge] = field(default_factory=list) + + def add_node(self, node: BuildGraphNode) -> None: + if node.id not in {n.id for n in self.nodes}: + self.nodes.append(node) + + def add_edge(self, edge: BuildGraphEdge) -> None: + key = (edge.source, edge.target, edge.relationship, tuple(sorted(edge.metadata.items()))) + keys = { + (e.source, e.target, e.relationship, tuple(sorted(e.metadata.items()))) + for e in self.edges + } + if key not in keys: + self.edges.append(edge) + + def to_dict(self) -> dict[str, object]: + return { + "nodes": [ + { + "id": n.id, + "kind": n.kind, + "label": n.label, + "metadata": n.metadata, + } + for n in sorted(self.nodes, key=lambda x: x.id) + ], + "edges": [ + { + "source": e.source, + "target": e.target, + "relationship": e.relationship, + "metadata": e.metadata, + } + for e in sorted( + self.edges, + key=lambda x: (x.source, x.target, x.relationship, sorted(x.metadata.items())), + ) + ], + } + + def summary(self) -> dict[str, object]: + relationship_counts: dict[str, int] = {} + for edge in self.edges: + relationship_counts[edge.relationship] = relationship_counts.get(edge.relationship, 0) + 1 + return { + "node_count": len(self.nodes), + "edge_count": len(self.edges), + "relationship_counts": dict(sorted(relationship_counts.items())), + } + + +def _kind_for_identity(identity: str) -> str: + value = identity.lower() + if "moduledefinition" in value or "/module/" in value: + return "abstract_design" + if "engineered" in value or "region" in value: + return "engineered_region" + if "plasmid" in value: + return "plasmid" + if "strain" in value: + return "strain" + if "plate" in value: + return "plate" + return "part" + + +def build_graph(result: FullBuildResult) -> BuildGraph: + graph = BuildGraph() + + for stage_result in result.stage_results: + stage_node_id = f"stage_result:{stage_result.id}" + graph.add_node(BuildGraphNode(id=stage_node_id, kind="stage_result", label=stage_result.stage.value)) + for request_id in sorted(stage_result.request_ids): + graph.add_node(BuildGraphNode(id=f"request:{request_id}", kind="abstract_design", label=request_id)) + graph.add_edge(BuildGraphEdge(source=f"request:{request_id}", target=stage_node_id, relationship="requires")) + for product in stage_result.products: + graph.add_node(BuildGraphNode(id=product.identity, kind=_kind_for_identity(product.identity), label=product.display_id)) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=product.identity, relationship="produces")) + for missing in stage_result.missing_inputs: + node_id = f"missing:{missing.missing_identity}" + graph.add_node(BuildGraphNode(id=node_id, kind="missing_input", label=missing.missing_display_id, metadata={"kind": missing.missing_kind})) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=node_id, relationship="blocks", metadata={"required_stage": str(missing.required_stage)})) + for approval in stage_result.required_approvals: + approval_id = f"approval:{approval.process}" + graph.add_node(BuildGraphNode(id=approval_id, kind="approval", label=approval.process, metadata={"status": approval.status.value})) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=approval_id, relationship="requires")) + + for product in result.final_products: + graph.add_node(BuildGraphNode(id=product.identity, kind=_kind_for_identity(product.identity), label=product.display_id)) + + for missing in result.missing_inputs: + graph.add_node(BuildGraphNode(id=f"missing:{missing.missing_identity}", kind="missing_input", label=missing.missing_display_id, metadata={"kind": missing.missing_kind})) + + for approval in result.required_approvals: + graph.add_node(BuildGraphNode(id=f"approval:{approval.process}", kind="approval", label=approval.process, metadata={"status": approval.status.value})) + + return graph diff --git a/src/buildcompiler/reporting/report.py b/src/buildcompiler/reporting/report.py new file mode 100644 index 0000000..895dda1 --- /dev/null +++ b/src/buildcompiler/reporting/report.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field +from typing import Any + +from buildcompiler.domain import BuildStatus, FullBuildResult +from buildcompiler.reporting.graph import BuildGraph, build_graph + + +@dataclass +class StageReportSection: + stage: str + status: str + request_ids: list[str] + product_count: int + missing_input_count: int + approval_count: int + warning_count: int + logs: list[str] = field(default_factory=list) + + +@dataclass +class RouteReport: + source_stage_result_id: str + selected: bool + route: dict[str, Any] + + +@dataclass +class RecommendedAction: + code: str + message: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class DependencyChainStep: + source: str + relationship: str + target: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildReport: + status: BuildStatus + executive_summary: str + stage_sections: list[StageReportSection] + selected_routes: list[RouteReport] + rejected_alternatives: list[RouteReport] + missing_inputs: list[dict[str, Any]] + required_approvals: list[dict[str, Any]] + warnings: list[dict[str, Any]] + next_actions: list[RecommendedAction] + dependency_chain: list[DependencyChainStep] + graph_summary: dict[str, Any] + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["status"] = self.status.value + return data + + def to_json(self) -> str: + return json.dumps(self.to_dict(), sort_keys=True) + + def to_markdown(self) -> str: + return "\n".join([ + "# Build Report", + f"- Status: `{self.status.value}`", + f"- Stage sections: `{len(self.stage_sections)}`", + f"- Selected routes: `{len(self.selected_routes)}`", + f"- Rejected alternatives: `{len(self.rejected_alternatives)}`", + f"- Missing inputs: `{len(self.missing_inputs)}`", + f"- Required approvals: `{len(self.required_approvals)}`", + f"- Warnings: `{len(self.warnings)}`", + "", + "## Executive Summary", + self.executive_summary, + ]) + + +def _recommended_actions(result: FullBuildResult) -> list[RecommendedAction]: + actions: list[RecommendedAction] = [] + for missing in result.missing_inputs: + kind = missing.missing_kind + if kind == "engineered_region": + actions.append(RecommendedAction("build_lvl1_engineered_region", "Build the missing engineered region through assembly level 1.", {"missing_identity": missing.missing_identity})) + elif kind in {"promoter", "rbs", "cds", "terminator"}: + actions.append(RecommendedAction("run_domestication", "Run domestication for missing part inputs.", {"missing_kind": kind, "missing_identity": missing.missing_identity})) + elif kind in {"backbone", "restriction_enzyme", "ligase", "reagent"}: + actions.append(RecommendedAction("provide_inventory_or_purchase", "Add missing inventory material or enable explicit purchase support.", {"missing_kind": kind, "missing_identity": missing.missing_identity})) + for approval in result.required_approvals: + actions.append(RecommendedAction("grant_required_approval", f"Grant required approval for process '{approval.process}'.", {"process": approval.process})) + for warning in result.warnings: + actions.append(RecommendedAction("inspect_warning", f"Inspect warning {warning.code} for details.", {"code": warning.code})) + # deterministic de-dup + unique: dict[tuple[str, str, str], RecommendedAction] = {} + for action in actions: + meta = json.dumps(action.metadata, sort_keys=True) + unique[(action.code, action.message, meta)] = action + return [unique[k] for k in sorted(unique)] + + +def build_report(result: FullBuildResult, graph: BuildGraph | None = None) -> BuildReport: + report_graph = graph or build_graph(result) + stage_sections = [ + StageReportSection( + stage=sr.stage.value, + status=sr.status.value, + request_ids=sorted(sr.request_ids), + product_count=len(sr.products), + missing_input_count=len(sr.missing_inputs), + approval_count=len(sr.required_approvals), + warning_count=len(sr.warnings), + logs=list(sr.logs), + ) + for sr in result.stage_results + ] + selected_routes: list[RouteReport] = [] + rejected: list[RouteReport] = [] + for sr in result.stage_results: + artifacts = sr.protocol_artifacts or {} + sel = artifacts.get("selected_route") + if isinstance(sel, dict): + selected_routes.append(RouteReport(sr.id, True, sel)) + for route in artifacts.get("rejected_routes", []) or []: + if isinstance(route, dict): + rejected.append(RouteReport(sr.id, False, route)) + + blocker_summary = f"{len(result.missing_inputs)} missing inputs and {len(result.required_approvals)} required approvals" + if result.status == BuildStatus.FAILED: + executive_summary = ( + f"Build failed with {blocker_summary}." + if result.missing_inputs or result.required_approvals + else "Build failed. Review stage logs and warnings for the root cause." + ) + elif result.missing_inputs or result.required_approvals: + executive_summary = f"Build is blocked by {blocker_summary}." + else: + executive_summary = "Build completed without unresolved blockers." + dependency_chain = [ + DependencyChainStep(e.source, e.relationship, e.target, dict(e.metadata)) + for e in sorted(report_graph.edges, key=lambda x: (x.source, x.target, x.relationship)) + if e.relationship in {"blocks", "requires", "produces", "satisfies", "transforms", "plates"} + ] + return BuildReport( + status=result.status, + executive_summary=executive_summary, + stage_sections=stage_sections, + selected_routes=selected_routes, + rejected_alternatives=rejected, + missing_inputs=[asdict(x) | {"source_stage": x.source_stage.value, "required_stage": str(x.required_stage)} for x in result.missing_inputs], + required_approvals=[asdict(x) | {"status": x.status.value} for x in result.required_approvals], + warnings=[asdict(x) | {"stage": x.stage.value if x.stage else None} for x in result.warnings], + next_actions=_recommended_actions(result), + dependency_chain=dependency_chain, + graph_summary=report_graph.summary(), + ) diff --git a/src/buildcompiler/reporting/summary.py b/src/buildcompiler/reporting/summary.py new file mode 100644 index 0000000..a6a9e5a --- /dev/null +++ b/src/buildcompiler/reporting/summary.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from typing import Any + +from buildcompiler.domain import BuildStatus, FullBuildResult + + +@dataclass +class BuildSummary: + status: BuildStatus + final_product_count: int + missing_input_count: int + required_approval_count: int + warning_count: int + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["status"] = self.status.value + return data + + def to_json(self) -> str: + return json.dumps(self.to_dict(), sort_keys=True) + + def to_markdown(self) -> str: + return "\n".join( + [ + "# Build Summary", + f"- Status: `{self.status.value}`", + f"- Final products: `{self.final_product_count}`", + f"- Missing inputs: `{self.missing_input_count}`", + f"- Required approvals: `{self.required_approval_count}`", + f"- Warnings: `{self.warning_count}`", + ] + ) + + +def build_summary(result: FullBuildResult) -> BuildSummary: + return BuildSummary( + status=result.status, + final_product_count=len(result.final_products), + missing_input_count=len(result.missing_inputs), + required_approval_count=len(result.required_approvals), + warning_count=len(result.warnings), + ) diff --git a/src/buildcompiler/robotutils.py b/src/buildcompiler/robotutils.py index 5b91cf9..6db50d6 100644 --- a/src/buildcompiler/robotutils.py +++ b/src/buildcompiler/robotutils.py @@ -1,10 +1,251 @@ import sbol2 import json +import csv import shutil import subprocess import tempfile import zipfile from pathlib import Path +from typing import Any, Dict, List + + +def load_json_or_dict(value): + """Load a JSON file path/string into a dict, or return dict-like input as-is.""" + if isinstance(value, dict): + return value + if isinstance(value, Path): + return json.loads(value.read_text(encoding="utf-8")) + if isinstance(value, str): + path = Path(value) + if path.exists(): + return json.loads(path.read_text(encoding="utf-8")) + return json.loads(value) + raise ValueError("Expected dict, JSON string, or JSON file path.") + + +def normalize_plating_input(transformation_results, doc=None): + """Normalize transformation/plating inputs to a deterministic list of entries.""" + payload = load_json_or_dict(transformation_results) + normalized = [] + + if isinstance(payload, dict) and isinstance(payload.get("sbol_artifacts"), list): + for idx, artifact in enumerate(payload["sbol_artifacts"], start=1): + if not isinstance(artifact, dict): + continue + impl_uri = artifact.get("transformed_strain_implementation") + module_uri = artifact.get("transformed_strain_module") + if not impl_uri and not module_uri: + continue + normalized.append( + { + "order": idx, + "well_hint": None, + "source_impl_uri": impl_uri, + "strain_module_uri": module_uri, + } + ) + elif isinstance(payload, dict) and isinstance(payload.get("strain_locations"), dict): + for idx, well in enumerate(sorted(payload["strain_locations"]), start=1): + impl_uri = payload["strain_locations"][well] + normalized.append( + { + "order": idx, + "well_hint": well, + "source_impl_uri": impl_uri, + "strain_module_uri": None, + } + ) + elif isinstance(payload, dict) and isinstance( + payload.get("bacterium_locations"), dict + ): + for idx, well in enumerate(sorted(payload["bacterium_locations"]), start=1): + impl_uri = payload["bacterium_locations"][well] + normalized.append( + { + "order": idx, + "well_hint": well, + "source_impl_uri": impl_uri, + "strain_module_uri": None, + } + ) + else: + raise ValueError( + "Unsupported plating input shape. Expected transformation output, " + "strain_locations, or bacterium_locations." + ) + + if doc is not None: + for item in normalized: + if item["strain_module_uri"] is None and item["source_impl_uri"]: + impl = doc.find(item["source_impl_uri"]) + if impl is not None and getattr(impl, "built", None): + item["strain_module_uri"] = impl.built + + if not normalized: + raise ValueError("No transformed strains found in plating input.") + + return sorted(normalized, key=lambda x: x["order"]) + + +def generate_96_well_positions(limit=96): + wells = [f"{row}{column}" for row in "ABCDEFGH" for column in range(1, 13)] + if limit < 0: + raise ValueError("limit must be non-negative") + return wells[:limit] + + +def write_plate_map_json(path, data): + path = Path(path) + path.write_text(json.dumps(data, indent=2), encoding="utf-8") + return path + + +def write_plate_map_csv(path, rows: List[Dict[str, Any]]): + path = Path(path) + fields = [ + "well", + "source_transformed_strain_implementation", + "strain_module", + "plated_strain_implementation", + "strain_display_name", + ] + with path.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow( + { + "well": row.get("well"), + "source_transformed_strain_implementation": row.get( + "source_transformed_strain_implementation" + ), + "strain_module": row.get("strain_module"), + "plated_strain_implementation": row.get( + "plated_strain_implementation" + ), + "strain_display_name": row.get("strain_display_name"), + } + ) + return path + + +def write_manual_plating_protocol(path, plate_id, plate_rows, advanced_params): + path = Path(path) + params = advanced_params or {} + table_lines = [ + "| Well | Source transformed strain implementation | Plated strain implementation | Strain module |", + "|---|---|---|---|", + ] + for row in plate_rows: + table_lines.append( + f"| {row['well']} | {row.get('source_transformed_strain_implementation','')} " + f"| {row.get('plated_strain_implementation','')} | {row.get('strain_module','')} |" + ) + + param_lines = "\n".join([f"- **{k}**: {v}" for k, v in sorted(params.items())]) or "- (none)" + strain_lines = "\n".join( + [ + f"- {row.get('strain_display_name', row.get('source_transformed_strain_implementation'))}" + for row in plate_rows + ] + ) + content = ( + "# BuildCompiler Plating Protocol\n\n" + f"## Plate\n- Plate ID: `{plate_id}`\n- Protocol type: `manual`\n\n" + "## Input transformed strains\n" + f"{strain_lines}\n\n" + "## Parameters\n" + f"{param_lines}\n\n" + "## 96-well plate map\n" + f"{chr(10).join(table_lines)}\n\n" + "## Steps\n" + "1. Prepare one sterile solid-media 96-well plate.\n" + "2. Label the plate with the plate ID and date.\n" + "3. Transfer each transformed strain to the destination well shown in the map.\n" + "4. Incubate according to lab defaults or parameters above.\n" + ) + path.write_text(content, encoding="utf-8") + return path + + +def write_plating_protocol_script(path, plating_data, advanced_params): + path = Path(path) + script = ( + "from pudu.plating import Plating\n" + "from opentrons import protocol_api\n\n" + "metadata = {\n" + ' "protocolName": "BuildCompiler Plating",\n' + ' "author": "BuildCompiler",\n' + ' "description": "Automated plating protocol generated from BuildCompiler transformation results",\n' + ' "apiLevel": "2.21",\n' + "}\n\n" + f"PLATING_DATA = {json.dumps(plating_data, indent=4)}\n" + f"ADVANCED_PARAMS = {json.dumps(advanced_params or {}, indent=4)}\n\n" + "def run(protocol: protocol_api.ProtocolContext):\n" + " plating = Plating(\n" + " plating_data=PLATING_DATA,\n" + " json_params=ADVANCED_PARAMS,\n" + " )\n" + " plating.run(protocol)\n" + ) + path.write_text(script, encoding="utf-8") + return path + + +def run_opentrons_script_to_zip( + opentrons_script_path, + plating_json_path, + zip_name=None, + overwrite=False, +): + script_path = Path(opentrons_script_path).resolve() + json_path = Path(plating_json_path).resolve() + if not script_path.exists(): + raise FileNotFoundError(f"Opentrons script not found: {script_path}") + if not json_path.exists(): + raise FileNotFoundError(f"JSON file not found: {json_path}") + + out_zip = script_path.parent / (zip_name or f"{script_path.stem}_simulation.zip") + if out_zip.exists() and not overwrite: + stem = out_zip.stem + suffix = out_zip.suffix + i = 1 + while True: + candidate = out_zip.parent / f"{stem}_{i}{suffix}" + if not candidate.exists(): + out_zip = candidate + break + i += 1 + + with tempfile.TemporaryDirectory() as tmpdirname: + tmpdir = Path(tmpdirname) + tmp_script = tmpdir / script_path.name + tmp_json = tmpdir / json_path.name + shutil.copy2(script_path, tmp_script) + shutil.copy2(json_path, tmp_json) + + proc = subprocess.run( + ["opentrons_simulate", str(tmp_script)], + capture_output=True, + text=True, + cwd=tmpdir, + ) + (tmpdir / "simulate_stdout.txt").write_text( + proc.stdout or "", encoding="utf-8", errors="replace" + ) + (tmpdir / "simulate_stderr.txt").write_text( + proc.stderr or "", encoding="utf-8", errors="replace" + ) + (tmpdir / "simulate_returncode.txt").write_text( + str(proc.returncode), encoding="utf-8" + ) + + with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for file_path in tmpdir.rglob("*"): + if file_path.is_file(): + zf.write(file_path, arcname=file_path.relative_to(tmpdir)) + + return out_zip def assembly_plan_RDF_to_JSON(file, output_path: str | Path | None = None): if isinstance(file, sbol2.Document): diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py new file mode 100644 index 0000000..7539882 --- /dev/null +++ b/src/buildcompiler/sbol/__init__.py @@ -0,0 +1,16 @@ +"""SBOL package exports for clean architecture contracts.""" + +from .assembly import AssemblyJob, AssemblySbolResult, AssemblyService +from .domestication import DomesticationJob, DomesticationSbolResult, DomesticationService +from .resolver import PullPolicy, SbolResolver + +__all__ = [ + "AssemblyJob", + "AssemblySbolResult", + "AssemblyService", + "DomesticationJob", + "DomesticationSbolResult", + "DomesticationService", + "PullPolicy", + "SbolResolver", +] diff --git a/src/buildcompiler/sbol/assembly.py b/src/buildcompiler/sbol/assembly.py new file mode 100644 index 0000000..fa877d9 --- /dev/null +++ b/src/buildcompiler/sbol/assembly.py @@ -0,0 +1,185 @@ +"""SBOL assembly service wrapping legacy Golden Gate behavior.""" + +from dataclasses import dataclass, field + +import sbol2 + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) +from buildcompiler.sbol2build import Assembly + + +@dataclass +class _LegacyPlasmidAdapter: + plasmid_definition: sbol2.ComponentDefinition + plasmid_implementations: list[sbol2.Implementation] + + +@dataclass +class AssemblyJob: + """Normalized assembly inputs plus source/target SBOL documents.""" + + stage: BuildStage + product_identity: str + product_display_id: str + part_plasmids: list[IndexedPlasmid] + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: sbol2.Document + target_document: sbol2.Document + include_extracted_parts: bool = False + + +@dataclass +class AssemblySbolResult: + """Assembly output contract: normalized products plus stage document.""" + + products: list[IndexedPlasmid] + stage_document: sbol2.Document + activity_identity: str + logs: list[str] = field(default_factory=list) + + +class AssemblyService: + """Service wrapper that preserves legacy assembly internals behind normalized contracts.""" + + def run(self, job: AssemblyJob) -> AssemblySbolResult: + if not job.part_plasmids: + raise ValueError("AssemblyJob.part_plasmids must contain at least one plasmid") + + legacy_parts = [ + self._record_to_legacy_plasmid(record, job.source_document, "part_plasmids") + for record in job.part_plasmids + ] + legacy_backbone = self._record_to_legacy_plasmid( + IndexedPlasmid( + identity=job.backbone.identity, + display_id=job.backbone.display_id, + name=job.backbone.name, + metadata=job.backbone.metadata, + sbol_component=job.backbone.sbol_component, + ), + job.source_document, + "backbone", + ) + + restriction_impl = self._implementation_from_record( + job.restriction_enzyme, job.source_document + ) + ligase_impl = self._implementation_from_record(job.ligase, job.source_document) + + composite_prefix = job.product_display_id or job.product_identity.split("/")[-1] + legacy_assembly = Assembly( + part_plasmids=legacy_parts, + backbone_plasmid=legacy_backbone, + restriction_enzyme=restriction_impl, + ligase=ligase_impl, + source_document=job.source_document, + final_document=job.target_document, + composite_prefix=composite_prefix, + ) + legacy_products, final_doc = legacy_assembly.run( + include_extracted_parts=job.include_extracted_parts + ) + + products = [ + self._indexed_product_from_legacy_product(plasmid, job) + for plasmid in legacy_products + ] + logs = [ + f"Assembled {len(products)} product(s) at stage {job.stage.value}.", + f"Assembly activity: {legacy_assembly.assembly_activity.identity}", + ] + + return AssemblySbolResult( + products=products, + stage_document=final_doc, + activity_identity=legacy_assembly.assembly_activity.identity, + logs=logs, + ) + + def _record_to_legacy_plasmid( + self, + record: IndexedPlasmid, + source_document: sbol2.Document, + field_name: str, + ) -> _LegacyPlasmidAdapter: + component = self._component_from_record(record, source_document, field_name) + implementation = self._implementation_from_plasmid_record(record, source_document) + return _LegacyPlasmidAdapter(component, [implementation]) + + def _component_from_record( + self, + record: IndexedPlasmid, + source_document: sbol2.Document, + field_name: str, + ) -> sbol2.ComponentDefinition: + component = record.sbol_component or source_document.find(record.identity) + if component is None: + raise ValueError( + f"Missing SBOL ComponentDefinition for {field_name} record {record.identity}" + ) + if not isinstance(component, sbol2.ComponentDefinition): + raise ValueError( + f"{field_name} record {record.identity} must resolve to sbol2.ComponentDefinition" + ) + return component + + def _implementation_from_plasmid_record( + self, record: IndexedPlasmid, source_document: sbol2.Document + ) -> sbol2.Implementation: + impl_identity = record.metadata.get("implementation_identity") + implementation = source_document.find(impl_identity) if impl_identity else None + + if implementation is None: + component = self._component_from_record(record, source_document, "plasmid") + matches = [ + impl + for impl in source_document.implementations + if isinstance(impl, sbol2.Implementation) and impl.built == component.identity + ] + implementation = matches[0] if matches else None + + if implementation is None: + raise ValueError( + f"Missing SBOL Implementation for plasmid {record.identity}; " + "set metadata['implementation_identity'] or include implementation in source_document" + ) + return implementation + + def _implementation_from_record( + self, record: IndexedReagent, source_document: sbol2.Document + ) -> sbol2.Implementation: + impl_identity = record.metadata.get("implementation_identity") or record.identity + implementation = source_document.find(impl_identity) + if not isinstance(implementation, sbol2.Implementation): + raise ValueError( + "Missing SBOL Implementation for reagent " + f"{record.identity}; expected metadata['implementation_identity'] or identity to resolve" + ) + return implementation + + def _indexed_product_from_legacy_product( + self, product, job: AssemblyJob + ) -> IndexedPlasmid: + component = product.plasmid_definition + return IndexedPlasmid( + identity=component.identity, + display_id=component.displayId, + name=component.name, + state=MaterialState.GENERATED, + roles=list(component.roles), + metadata={ + "source_stage": job.stage.value, + "source_product_identity": job.product_identity, + "source_product_display_id": job.product_display_id, + "assembly_activity_identity": product.plasmid_implementations[0].wasGeneratedBy, + }, + sbol_component=component, + ) diff --git a/src/buildcompiler/sbol/domestication.py b/src/buildcompiler/sbol/domestication.py new file mode 100644 index 0000000..2e1014f --- /dev/null +++ b/src/buildcompiler/sbol/domestication.py @@ -0,0 +1,83 @@ +"""SBOL domestication service.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +import sbol2 + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent, MaterialState + + +@dataclass +class DomesticationJob: + part_identity: str + part_display_id: str | None + part_component: Any + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: Any + target_document: Any + sequence_edit_proposals: list[Any] = field(default_factory=list) + + +@dataclass +class DomesticationSbolResult: + product: IndexedPlasmid + stage_document: Any + artifacts: dict[str, Any] = field(default_factory=dict) + logs: list[str] = field(default_factory=list) + + +class DomesticationService: + def run(self, job: DomesticationJob) -> DomesticationSbolResult: + component = self._ensure_component(job.part_component) + product_identity = f"{component.identity}/domesticated" + product_display_id = f"{job.part_display_id or component.displayId or component.identity.rsplit('/', 1)[-1]}_lvl0" + + product_component = sbol2.ComponentDefinition(product_identity) + product_component.displayId = product_display_id + product_component.name = f"Domesticated {component.displayId or component.identity.rsplit('/', 1)[-1]}" + product_component.roles = list(component.roles) + job.target_document.addComponentDefinition(product_component) + + implementation_identity = f"{product_identity}_implementation" + product_implementation = sbol2.Implementation(implementation_identity) + product_implementation.built = product_component.identity + job.target_document.addImplementation(product_implementation) + + metadata = { + "source_stage": "domestication", + "source_part_identity": job.part_identity, + "insert_identities": [job.part_identity], + "implementation_identity": product_implementation.identity, + "backbone_identity": job.backbone.identity, + "restriction_enzyme": { + "identity": job.restriction_enzyme.identity, + "name": job.restriction_enzyme.name, + }, + "ligase": {"identity": job.ligase.identity, "name": job.ligase.name}, + "sequence_edit_proposals": [proposal.__dict__.copy() for proposal in job.sequence_edit_proposals], + } + product = IndexedPlasmid( + identity=product_component.identity, + display_id=product_display_id, + name=product_component.name, + state=MaterialState.GENERATED, + roles=list(component.roles), + metadata=metadata, + sbol_component=product_component, + ) + return DomesticationSbolResult( + product=product, + stage_document=job.target_document, + artifacts={"domestication": metadata}, + logs=[f"Generated domesticated lvl0 product {product_identity}."], + ) + + def _ensure_component(self, component: Any) -> sbol2.ComponentDefinition: + if not isinstance(component, sbol2.ComponentDefinition): + raise ValueError("DomesticationJob.part_component must be an sbol2.ComponentDefinition") + return component diff --git a/src/buildcompiler/sbol/resolver.py b/src/buildcompiler/sbol/resolver.py new file mode 100644 index 0000000..3618983 --- /dev/null +++ b/src/buildcompiler/sbol/resolver.py @@ -0,0 +1,74 @@ +"""SBOL document resolver with deterministic pull policy.""" + +from __future__ import annotations + +from enum import Enum +from typing import Any, Callable + +import sbol2 + + +class PullPolicy(str, Enum): + """Resolver behavior for remote pull attempts.""" + + NEVER = "never" + MISSING_ONLY = "missing_only" + ALWAYS_REFRESH = "always_refresh" + + +class SbolResolver: + """Resolve SBOL objects by identity from a local document with optional pull fallback.""" + + def __init__( + self, + document: sbol2.Document, + *, + pull_policy: PullPolicy = PullPolicy.MISSING_ONLY, + pull_client: Callable[[str], Any] | None = None, + ) -> None: + self.document = document + self.pull_policy = pull_policy + self.pull_client = pull_client + + def maybe_pull(self, identity: str) -> Any | None: + if self.pull_policy == PullPolicy.NEVER: + return None + if self.pull_client is None: + return None + return self.pull_client(identity) + + def _get(self, identity: str, expected_type: type) -> Any: + if self.pull_policy == PullPolicy.ALWAYS_REFRESH: + pulled = self.maybe_pull(identity) + if isinstance(pulled, expected_type): + return pulled + + obj = self.document.find(identity) + if isinstance(obj, expected_type): + return obj + + if self.pull_policy == PullPolicy.MISSING_ONLY: + pulled = self.maybe_pull(identity) + if isinstance(pulled, expected_type): + return pulled + obj = self.document.find(identity) + if isinstance(obj, expected_type): + return obj + + raise LookupError( + f"Could not resolve {expected_type.__name__} with identity '{identity}'" + ) + + def get_component(self, identity: str) -> sbol2.ComponentDefinition: + return self._get(identity, sbol2.ComponentDefinition) + + def get_module(self, identity: str) -> sbol2.ModuleDefinition: + return self._get(identity, sbol2.ModuleDefinition) + + def get_combinatorial_derivation( + self, identity: str + ) -> sbol2.CombinatorialDerivation: + return self._get(identity, sbol2.CombinatorialDerivation) + + def get_implementation(self, identity: str) -> sbol2.Implementation: + return self._get(identity, sbol2.Implementation) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index fc731bf..00c01f8 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -10,7 +10,6 @@ DNA_TYPES, ENGINEERED_INSERT, ENGINEERED_PLASMID, - ENGINEERED_REGION, FIVE_PRIME_OVERHANG, FUSION_SITES, LINEAR, @@ -21,7 +20,7 @@ THREE_PRIME_OVERHANG, ) -sbol2.Config.setHomespace("https://SBOL2Build.org") +sbol2.Config.setHomespace("http://buildcompiler.org") sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True) sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False) @@ -29,48 +28,68 @@ class Assembly: """Creates an Assembly Plan. - :param name: Name of the assembly plan ModuleDefinition. - :param part_plasmids: Parts in backbone to be assembled. - :param plasmid_acceptor_backbone: Backbone in which parts are inserted on the assembly. - :param restriction_enzyme: Restriction enzyme name used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI' - :param document: SBOL Document where the assembly plan will be created. + :param part_plasmids: List of part-in-backbone plasmids to be assembled. + :param backbone_plasmid: Acceptor backbone into which parts are inserted. + :param restriction_enzyme: SBOL Implementation representing the restriction enzyme + (e.g. BsaI) used to digest parts during assembly. + :param ligase: SBOL Implementation representing the ligase (e.g. T4) used to + ligate digested parts. + :param source_document: SBOL Document containing the source part/plasmid definitions. + :param final_document: SBOL Document where assembled composite plasmid definitions + will be written. + :param composite_prefix: Prefix used when naming composite plasmid definitions. + Defaults to 'composite'. + """ def __init__( # TODO add fields for activity/agent/plan self, part_plasmids: List[Plasmid], backbone_plasmid: Plasmid, - restriction_enzyme: sbol2.Implementation, # TODO search for implementation in document, or domesticate the RE + restriction_enzyme: sbol2.Implementation, ligase: sbol2.Implementation, - document: sbol2.Document, + source_document: sbol2.Document, + final_document: sbol2.Document, + composite_prefix: str = "composite", ): self.part_plasmids = part_plasmids self.backbone = backbone_plasmid self.restriction_enzyme = restriction_enzyme self.ligase = ligase self.extracted_parts = [] # list of tuples [ComponentDefinition, Sequence] - self.source_document = document - self.final_document = sbol2.Document() - self.assembly_activity = initialize_assembly_activity() + self.source_document = source_document + self.final_document = final_document + self.composite_prefix = composite_prefix + self.assembly_activity = self.initialize_assembly_activity() self.composites = [] def run( - self, include_extracted_parts=False - ) -> List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]: - """Runs full assembly simulation. - - `document` parameter of golden_gate_assembly_plan object is updated by reference to include assembly plan ModuleDefinition and all related information. - - Runs :func:`part_digestion` for all `part_plasmids` and :func:`backbone_digestion` for `plasmid_acceptor_backbone` with `restriction_enzyme`. Then runs :func:`ligation` with these parts to form composites. - - :return: List of all composites generated, in the form of tuples of ComponentDefinition and Sequence. + self, include_extracted_parts: bool = False + ) -> Tuple[List[Plasmid], sbol2.Document]: + """Run the full Golden Gate assembly simulation. + + Executes the following steps in order: + + 1. Calls :func:`part_digestion` on each plasmid in ``part_plasmids`` using + ``restriction_enzyme``, appending extracted parts to ``source_document``. + 2. Calls :func:`backbone_digestion` on the first implementation of ``backbone``, + appending the linearised backbone to ``source_document``. + 3. Calls :func:`ligation` on all extracted parts and the backbone to produce + composite plasmid implementations, written to ``final_document``. + 4. Wraps each composite implementation in a :class:`Plasmid` object and returns + the full list alongside the populated ``final_document``. + + :param include_extracted_parts: If ``True``, extracted part and backbone + definitions are also written to ``final_document`` in addition to + ``source_document``. Defaults to ``False``. + :return: A tuple of (composite plasmids, final document), where composite + plasmids is a list of :class:`Plasmid` objects built from the ligated + implementations, and final document is the populated ``sbol2.Document`` + containing all assembly outputs. """ for plasmid in self.part_plasmids: - plasmid_impl = plasmid.plasmid_implementations[ - 0 - ] # TODO update with more sophisticated selection process? extracts_tuple_list, _ = part_digestion( - plasmid_impl, + plasmid, [self.restriction_enzyme], self.assembly_activity, self.source_document, @@ -80,9 +99,8 @@ def run( append_extracts_to_doc(extracts_tuple_list, self.final_document) self.extracted_parts.append(extracts_tuple_list[0][0]) - backbone_impl = self.backbone.plasmid_implementations[0] extracts_tuple_list, _ = backbone_digestion( - backbone_impl, + self.backbone, [self.restriction_enzyme], self.assembly_activity, self.source_document, @@ -96,6 +114,7 @@ def run( self.composites = ligation( self.extracted_parts, self.assembly_activity, + self.composite_prefix, self.source_document, self.final_document, self.ligase, @@ -116,6 +135,146 @@ def run( return composite_plasmid_objs, self.final_document + def initialize_assembly_activity(self): + activity = sbol2.Activity(f"{self.composite_prefix}_assembly") + + activity.name = "DNA Assembly" + activity.types = "http://sbols.org/v2#build" + + activity_association = sbol2.Association("assemble_") + + assembly_plan = sbol2.Plan("assembly_plan") + + assembly_plan.description = "MoClo DNA Assembly With Opentrons OT2" + + activity_association.plan = assembly_plan + + activity_agent = sbol2.Agent("BuildCompiler") + activity_association.agent = activity_agent + + activity.associations = [activity_association] + + return activity + + +class Transformation: + """Create SBOL transformation records from plasmid/chassis inputs. + + Mirrors the orchestration pattern used by :class:`Assembly`, but for + chemical transformation into a chassis strain. + """ + + def __init__( + self, + plasmids: List[Plasmid], + chassis_name: str, + source_document: sbol2.Document, + ): + self.plasmids = plasmids + self.chassis_name = chassis_name + self.source_document = source_document + + def _add_if_absent(self, obj): + if self.source_document.find(obj.identity) is None: + self.source_document.add(obj) + + def _get_or_create_chassis(self) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: + chassis_module = self.source_document.find(self.chassis_name) or sbol2.ModuleDefinition( + self.chassis_name + ) + chassis_module.name = self.chassis_name + self._add_if_absent(chassis_module) + + chassis_impl_id = f"{self.chassis_name}_impl" + chassis_impl = self.source_document.find(chassis_impl_id) or sbol2.Implementation( + chassis_impl_id + ) + chassis_impl.built = chassis_module.identity + self._add_if_absent(chassis_impl) + return chassis_module, chassis_impl + + def chemical_transformation(self) -> list[dict]: + chassis_module, chassis_impl = self._get_or_create_chassis() + outputs = [] + + for index, plasmid in enumerate(self.plasmids, start=1): + plasmid_def = plasmid.plasmid_definition + plasmid_impl = ( + plasmid.plasmid_implementations[0] + if plasmid.plasmid_implementations + else None + ) + if plasmid_impl is None: + plasmid_impl_id = f"{plasmid_def.displayId}_impl" + plasmid_impl = self.source_document.find(plasmid_impl_id) or sbol2.Implementation( + plasmid_impl_id + ) + plasmid_impl.built = plasmid_def.identity + self._add_if_absent(plasmid_impl) + + transform_id = f"transform_{plasmid_def.displayId}_{index}" + transformation_activity = sbol2.Activity(transform_id) + transformation_activity.name = ( + f"Transform {self.chassis_name} with {plasmid_def.displayId}" + ) + transformation_activity.types = "http://sbols.org/v2#build" + + chassis_usage = sbol2.Usage( + uri=f"{transform_id}_chassis_usage", + entity=chassis_impl.identity, + role="http://sbols.org/v2#build", + ) + plasmid_usage = sbol2.Usage( + uri=f"{transform_id}_plasmid_usage", + entity=plasmid_impl.identity, + role="http://sbols.org/v2#build", + ) + transformation_activity.usages = [chassis_usage, plasmid_usage] + + transformed_strain = sbol2.ModuleDefinition( + f"{self.chassis_name}_with_{plasmid_def.displayId}" + ) + transformed_strain.name = ( + f"{self.chassis_name} transformed with {plasmid_def.displayId}" + ) + chassis_module_ref = sbol2.Module( + uri=f"{transformed_strain.displayId}_chassis_module" + ) + chassis_module_ref.definition = chassis_module.identity + plasmid_fc = sbol2.FunctionalComponent( + uri=f"{transformed_strain.displayId}_plasmid_fc" + ) + plasmid_fc.definition = plasmid_def.identity + transformed_strain.modules = [chassis_module_ref] + transformed_strain.functionalComponents = [plasmid_fc] + + transformed_impl = sbol2.Implementation( + f"{transformed_strain.displayId}_impl" + ) + transformed_impl.built = transformed_strain.identity + transformed_impl.wasGeneratedBy = transformation_activity.identity + + for obj in ( + transformation_activity, + chassis_usage, + plasmid_usage, + transformed_strain, + chassis_module_ref, + plasmid_fc, + transformed_impl, + ): + self._add_if_absent(obj) + + outputs.append( + { + "transformation_activity": transformation_activity.identity, + "transformed_strain_module": transformed_strain.identity, + "transformed_strain_implementation": transformed_impl.identity, + } + ) + + return outputs + def rebase_restriction_enzyme(name: str, **kwargs) -> sbol2.ComponentDefinition: """Creates an ComponentDefinition Restriction Enzyme Component from rebase. @@ -298,44 +457,55 @@ def is_circular(obj: sbol2.ComponentDefinition) -> bool: def part_digestion( - reactant: sbol2.Implementation, + reactant: Plasmid, restriction_enzymes: List[sbol2.Implementation], assembly_activity: sbol2.Activity, document: sbol2.Document, -) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: - """Runs a simulated digestion on the top level sequence in the reactant ComponentDefinition or ModuleDefinition with the given restriciton enzymes, creating a extracted part ComponentDefinition, a digestion Interaction, and converts existing scars to 5' and 3' overhangs. - The product ComponentDefinition is assumed the digested part in this case. - - Written for use with the SBOL2.3 output of https://sbolcanvas.org - - :param reactant: Plasmid DNA to be digested as SBOL ComponentDefinition - :param restriction_enzymes: Restriction enzymes as :class:`sbol2.ComponentDefinition` - (generate with :func:`rebase_restriction_enzyme`). - :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participations - :param document: original SBOL2 document to be used to extract referenced objects. - :return: A tuple of a list ComponentDefinitions and Sequences, and an assembly plan ModuleDefinition. +) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.Activity]: + """Simulate restriction digestion of a part plasmid and extract the insert. + + Uses PyDNA to cut the reactant sequence, then constructs SBOL representations + of the extracted part, its 5' and 3' overhangs, and any derived scar sequences. + Each enzyme and the reactant implementation are recorded as usages on + ``assembly_activity``. + + Expects the reactant to be circular with 2 digest products, or linear with 3 + (backbone | part | backbone). The shorter circular product or middle linear + product is taken as the extracted insert. + + :param reactant: Part-in-backbone plasmid to digest. + :param restriction_enzymes: Restriction enzyme implementations; the corresponding + ``ComponentDefinition.name`` must match a PyDNA/ReBase enzyme name (e.g. ``'BsaI'``). + :param assembly_activity: SBOL Activity to record reactant and enzyme usages on. + :param document: Source SBOL document used to resolve referenced definitions and sequences. + :return: A tuple of (extracts, activity), where extracts is a list of + ``(ComponentDefinition, Sequence)`` pairs covering the extracted part, + overhangs, and scar definitions, and activity is the updated ``assembly_activity``. + :raises TypeError: If the reactant has no recognised DNA type. + :raises ValueError: If the reactant does not have exactly one sequence, or if + the number of digest products is unsupported for the reactant topology. """ - - reactant_component_definition = document.get(reactant.built) + reactant_impl = reactant.plasmid_implementations[0] + reactant_component_definition = reactant.plasmid_definition reactant_displayId = reactant_component_definition.displayId types = set(reactant_component_definition.types or []) if not types.intersection(DNA_TYPES): raise TypeError( - f"The reactant should have a DNA type. Types found: {reactant.types}." + f"The reactant should have a DNA type. Types found: {reactant_component_definition.types}." ) if len(reactant_component_definition.sequences) != 1: raise ValueError( - f"The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences" + f"The reactant needs to have precisely one sequence. The input reactant has {len(reactant_component_definition.sequences)} sequences" ) extracts_list = [] restriction_enzymes_pydna = [] assembly_activity.usages.add( sbol2.Usage( - uri=f"{reactant.displayId}", - entity=reactant.identity, + uri=f"{reactant_impl.displayId}", + entity=reactant_impl.identity, role="http://sbols.org/v2#build", ) ) @@ -497,8 +667,20 @@ def part_digestion( # find + add original component to product def & annotation for comp in reactant_component_definition.components: if comp.definition == original_part_def_URI: - prod_component_definition.components.add(comp) - part_extract_annotation.component = comp + new_comp = prod_component_definition.components.create(comp.displayId) + new_comp.definition = comp.definition + part_extract_annotation.component = new_comp + + original_cd = document.getComponentDefinition(comp.definition) + seq = document.get(original_cd.sequences[0]) + + new_seq = sbol2.Sequence( + uri=f"{reactant_component_definition.displayId}_extracted_part_seq", + elements=seq.elements, + encoding=seq.encoding, + ) + prod_component_definition.sequences.append(new_seq) + extracts_list.append((new_comp, new_seq)) prod_component_definition.sequenceAnnotations.add(three_prime_overhang_annotation) prod_component_definition.sequenceAnnotations.add(five_prime_overhang_annotation) @@ -510,24 +692,35 @@ def part_digestion( def backbone_digestion( - reactant: sbol2.Implementation, + reactant: Plasmid, restriction_enzymes: List[sbol2.Implementation], assembly_activity: sbol2.Activity, document: sbol2.Document, -) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: - """Runs a simulated digestion on the top level sequence in the reactant ComponentDefinition or ModuleDefinition with the given restriciton enzymes, creating an open backbone ComponentDefinition, a digestion Interaction, and converts existing scars to 5' and 3' overhangs. - The product ComponentDefinition is assumed the open backbone in this case. - - Written for use with the SBOL2.3 output of https://sbolcanvas.org - - :param reactant: DNA to be digested as SBOL ComponentDefinition or ModuleDefinition, usually a part_in_backbone. ComponentDefinition is the best-practice type for plasmids. - :param restriction_enzymes: Restriction enzymes as :class:`sbol2.ComponentDefinition` - (generate with :func:`rebase_restriction_enzyme`). - :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participations - :param document: original SBOL2 document to be used to extract referenced objects. - :return: A tuple of a list ComponentDefinitions and Sequences, and an assembly plan ModuleDefinition. +) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.Activity]: + """Simulate restriction digestion of a backbone plasmid and extract the linearised vector. + + Mirrors :func:`part_digestion` but targets the backbone: for a circular reactant + with 2 digest products the longer fragment is taken as the open backbone; for a + linear reactant with 3 products the outer prefix/suffix fragments are used. + The resulting open-backbone ``ComponentDefinition``, its 5' and 3' overhangs, and + any matched scar sequences are returned as SBOL objects. The reactant implementation + and each enzyme are recorded as usages on ``assembly_activity``. + + :param reactant: SBOL Implementation whose ``built`` URI resolves to the + backbone ``ComponentDefinition`` in ``document``. + :param restriction_enzymes: Restriction enzyme implementations; the corresponding + ``ComponentDefinition.name`` must match a PyDNA/ReBase enzyme name (e.g. ``'BsaI'``). + :param assembly_activity: SBOL Activity to record reactant and enzyme usages on. + :param document: Source SBOL document used to resolve referenced definitions and sequences. + :return: A tuple of (extracts, activity), where extracts is a list of + ``(ComponentDefinition, Sequence)`` pairs covering the open backbone, + overhangs, and scar definitions, and activity is the updated ``assembly_activity``. + :raises TypeError: If the reactant has no recognised DNA type. + :raises ValueError: If the reactant does not have exactly one sequence, or if + the number of digest products is unsupported for the reactant topology. """ - reactant_component_definition = document.get(reactant.built) + reactant_impl = reactant.plasmid_implementations[0] + reactant_component_definition = document.get(reactant_impl.built) reactant_displayId = reactant_component_definition.displayId types = set(reactant_component_definition.types or []) @@ -545,8 +738,8 @@ def backbone_digestion( assembly_activity.usages.add( sbol2.Usage( - uri=f"{reactant.displayId}", - entity=reactant.identity, + uri=f"{reactant_impl.displayId}", + entity=reactant_impl.identity, role="http://sbols.org/v2#build", ) ) @@ -591,7 +784,7 @@ def backbone_digestion( if len(digested_reactant) < 2 or len(digested_reactant) > 3: raise ValueError( - f"Not supported number of products. Found: {len(digested_reactant)}" + f"Not supported number of products. Found: {len(digested_reactant)} after digesting {reactant_displayId}" ) # TODO select them based on content rather than size. elif circular and len(digested_reactant) == 2: @@ -735,17 +928,21 @@ def number_to_suffix(n): def ligation( reactants: List[sbol2.ComponentDefinition], assembly_activity: sbol2.Activity, + composite_prefix: str, source_document: sbol2.Document, final_document: sbol2.Document, ligase: sbol2.Implementation, ) -> List[sbol2.Implementation]: """Ligates Components using base complementarity and creates product Components and a ligation Interaction. - :param reactants: DNA parts to be ligated as SBOL ModuleDefinition. + :param reactants: Extracted part and backbone ``ComponentDefinition`` objects to ligate. :param assembly_activity: SBOL activity to track assembly inputs & outputs - :param document: SBOL2 document containing all reactant ComponentDefinitions. - :param ligase: as SBOL Implementation - :return: List of all composites generated, in the form of tuples of ComponentDefinition and Sequence. + :param composite_prefix: Prefix used when naming composite ``ComponentDefinition`` + and ``Implementation`` identities. + :param source_document: SBOL Document containing all reactant definitions. + :param final_document: SBOL Document that receives composite definitions and implementations. + :param ligase: SBOL Implementation of the ligase (e.g. T4). + :return: List of ``sbol2.Implementation`` objects, one per composite plasmid generated. """ enzyme_definition = source_document.get(ligase.built) @@ -819,39 +1016,45 @@ def ligation( insert_sequence_uri = combination[0].sequences[0] insert_sequence = source_document.getSequence(insert_sequence_uri).elements remaining_parts = list(combination[1:]) + insert_3prime_match_id = None it = 1 while remaining_parts: remaining_parts_before = len(remaining_parts) for part in remaining_parts: # match insert sequence 5' to part 3' part_sequence_uri = part.sequences[0] + # check reverse match if ( source_document.getSequence(part_sequence_uri) - .elements[:fusion_site_length] + .elements[-fusion_site_length:] .lower() - == insert_sequence[-fusion_site_length:].lower() + == insert_sequence[:fusion_site_length].lower() ): - insert_sequence = ( - insert_sequence[:-fusion_site_length] - + source_document.getSequence(part_sequence_uri).elements - ) - list_of_parts_per_composite.append( - part - ) # add sequence annotation here, index based on insert_sequence - remaining_parts.remove(part) - # match insert sequence 3' to part 5' - elif ( + insert_3prime_match_id = part.identity + if ( source_document.getSequence(part_sequence_uri) - .elements[-fusion_site_length:] + .elements[:fusion_site_length] .lower() - == insert_sequence[:fusion_site_length].lower() + == insert_sequence[-fusion_site_length:].lower() ): - insert_sequence = ( - source_document.getSequence(part_sequence_uri).elements - + insert_sequence[fusion_site_length:] - ) - list_of_parts_per_composite.insert(0, part) - remaining_parts.remove(part) + if ( + len(remaining_parts) == 1 + and part.identity == insert_3prime_match_id + ): # check flag and match backbone 5' on final part 3' + insert_sequence = ( + insert_sequence[:-fusion_site_length] + + source_document.getSequence(part_sequence_uri).elements + ) + list_of_parts_per_composite.append(part) + remaining_parts.remove(part) + elif len(remaining_parts) > 1: + insert_sequence = ( + insert_sequence[:-fusion_site_length] + + source_document.getSequence(part_sequence_uri).elements + ) + list_of_parts_per_composite.append(part) + remaining_parts.remove(part) + # match backbone 5' to insert sequence 3', set flag remaining_parts_after = len(remaining_parts) if remaining_parts_before == remaining_parts_after: @@ -865,9 +1068,8 @@ def ligation( # transform list_of_parts_per_assembly into list of composites product_impl_list = [] - composite_number = 1 + composite_number = 0 - # TODO: use componentinstances to append "subcomponents" to each definition that is a composite component. all composites share the "subcomponents" for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C] # calculate sequence composite_sequence_str = "" @@ -944,15 +1146,25 @@ def ligation( source_document.getComponentDefinition(prev_three_prime) ) else: + anno_prefix = comp.displayId + + matching_anno_prefix = [ + a.displayId + for a in anno_list + if a.displayId.startswith(f"{comp.displayId}_") + ] + if matching_anno_prefix: + anno_prefix = f"{comp.displayId}_{len(matching_anno_prefix)}" + temp_extract_components.append(comp.definition) comp_location = sbol2.Range( - uri=f"{comp.displayId}_location", + uri=f"{anno_prefix}_location", start=len(composite_sequence_str) + fusion_site_length + 1, end=len(composite_sequence_str) + len(part_extract_sequence[:-4]), - ) # TODO check if seq len is correct + ) comp_anno = sbol2.SequenceAnnotation( - uri=f"{comp.displayId}_annotation" + uri=f"{anno_prefix}_annotation" ) comp_anno.locations.add(comp_location) anno_list.append(comp_anno) @@ -963,14 +1175,18 @@ def ligation( composite_sequence_str + part_extract_sequence[:-fusion_site_length] ) # needs a version for linear + suffix = f"_{composite_number}" if composite_number > 0 else "" + # create dna component and sequence composite_component_definition, composite_seq = ( dna_componentdefinition_with_sequence( - f"composite_{composite_number}", composite_sequence_str, molecule=True + f"{composite_prefix}{suffix}", + composite_sequence_str, + molecule=True, ) ) - composite_component_definition.name = f"composite_{composite_number}" - composite_component_definition.addRole(ENGINEERED_REGION) + composite_component_definition.name = f"{composite_prefix}{suffix}" + composite_component_definition.addRole(ENGINEERED_PLASMID) composite_component_definition.addType(CIRCULAR) prev_part_extract = None @@ -990,8 +1206,6 @@ def ligation( prev_part_extract = comp - # _create_precedes_restriction(composite_component_definition, prev_part_extract, composite_component_definition.components[0]) # final component precedes first component; defining circular order - composite_component_definition.sequenceAnnotations = anno_list composite_implementation = sbol2.Implementation( @@ -1000,6 +1214,10 @@ def ligation( composite_implementation.built = composite_component_definition.identity composite_implementation.wasGeneratedBy = assembly_activity.identity + source_document.add_list( + [composite_component_definition, composite_seq, composite_implementation] + ) + final_document.add_list( [composite_component_definition, composite_seq, composite_implementation] ) @@ -1043,28 +1261,6 @@ def add_object_to_doc( raise e -def initialize_assembly_activity(): - activity = sbol2.Activity("assembly") - - activity.name = "DNA Assembly" - activity.types = "http://sbols.org/v2#build" - - activity_association = sbol2.Association("assemble_") - - assembly_plan = sbol2.Plan("assembly_plan") - - assembly_plan.description = "MoClo DNA Assembly With Opentrons OT2" - - activity_association.plan = assembly_plan - - activity_agent = sbol2.Agent("BuildCompiler") - activity_association.agent = activity_agent - - activity.associations = [activity_association] - - return activity - - def _create_precedes_restriction( parent_definition: sbol2.ComponentDefinition, subject: sbol2.Component, diff --git a/src/buildcompiler/stages/__init__.py b/src/buildcompiler/stages/__init__.py new file mode 100644 index 0000000..bacbb7d --- /dev/null +++ b/src/buildcompiler/stages/__init__.py @@ -0,0 +1,7 @@ +"""Stage exports.""" + +from .assembly_lvl1 import AssemblyLvl1Stage +from .assembly_lvl2 import AssemblyLvl2Stage +from .domestication import DomesticationStage + +__all__ = ["AssemblyLvl1Stage", "AssemblyLvl2Stage", "DomesticationStage"] diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py new file mode 100644 index 0000000..04ada99 --- /dev/null +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -0,0 +1,239 @@ +"""Thin lvl1 assembly stage orchestration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import sbol2 + +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildWarning, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.inventory import CompatibilitySelector, Inventory +from buildcompiler.sbol import AssemblyJob, AssemblyService + + +class AssemblyLvl1Stage: + def __init__( + self, + *, + inventory: Inventory, + selector: CompatibilitySelector | None = None, + assembly_service: AssemblyService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + self.selector = selector or CompatibilitySelector( + inventory, options=self.options + ) + self.assembly_service = assembly_service or AssemblyService() + + def run( + self, + request: BuildRequest, + *, + source_document: sbol2.Document, + target_document: sbol2.Document, + ) -> StageResult: + constraints = request.constraints or {} + warnings = self._extract_warnings(request=request, constraints=constraints) + part_identities = self._extract_part_identities(constraints) + if not part_identities: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.FAILED, + request_ids=[request.id], + warnings=warnings, + logs=[ + "Missing ordered_part_identities/part_identities constraint for lvl1 assembly." + ], + ) + + route_selection = self.selector.select_lvl1_route( + request_id=request.id, + part_identities=part_identities, + constraints=constraints, + ) + route = route_selection.selected + if route is None: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + warnings=warnings, + logs=["No lvl1 route selected by CompatibilitySelector."], + ) + + missing_inputs: list[MissingBuildInput] = [] + for missing_identity in route.missing_part_identities: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=missing_identity, + missing_display_id=missing_identity.rsplit("/", 1)[-1], + missing_kind=self._infer_missing_kind(missing_identity), + required_stage=BuildStage.DOMESTICATION, + reason="No compatible lvl1 part plasmid found in inventory.", + ) + ) + + if route.backbone is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity="backbone", + missing_display_id=None, + missing_kind="backbone", + required_stage="fatal", + reason="No compatible lvl1 backbone found in inventory.", + ) + ) + + restriction_enzyme_name = self.options.reagents.default_restriction_enzyme + restriction_enzyme = self.inventory.find_restriction_enzyme( + restriction_enzyme_name + ) + if restriction_enzyme is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=restriction_enzyme_name, + missing_display_id=restriction_enzyme_name, + missing_kind="restriction_enzyme", + required_stage="fatal", + reason="Required restriction enzyme is missing from inventory.", + ) + ) + + ligase_name = self.options.reagents.default_ligase + ligase = self.inventory.find_ligase(ligase_name) + if ligase is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=ligase_name, + missing_display_id=ligase_name, + missing_kind="ligase", + required_stage="fatal", + reason="Required ligase is missing from inventory.", + ) + ) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + warnings=warnings, + logs=[ + f"Blocked lvl1 assembly for {request.id}; missing {len(missing_inputs)} required input(s)." + ], + ) + + product_identity = ( + constraints.get("product_identity") or request.source_identity + ) + product_display_id = ( + constraints.get("product_display_id") + or request.source_display_id + or product_identity.rsplit("/", 1)[-1] + ) + + assembly_result = self.assembly_service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity=product_identity, + product_display_id=product_display_id, + part_plasmids=list(route.selected_part_plasmids), + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ligase=ligase, + source_document=source_document, + target_document=target_document, + ) + ) + + for product in assembly_result.products: + insert_identities = list(product.metadata.get("insert_identities", [])) + if request.source_identity not in insert_identities: + insert_identities.append(request.source_identity) + product.metadata["insert_identities"] = insert_identities + self.inventory.add_generated_product(product) + + json_intermediate = assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=route.selected_part_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ) + + logs = [ + f"Selected lvl1 route with {len(route.selected_part_plasmids)} part plasmid(s).", + *assembly_result.logs, + ] + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=assembly_result.products, + warnings=warnings, + sbol_document=assembly_result.stage_document, + json_intermediate=json_intermediate, + logs=logs, + ) + + def _extract_part_identities(self, constraints: Mapping[str, Any]) -> list[str]: + ordered = constraints.get("ordered_part_identities") + if ordered: + return list(ordered) + planner_order = constraints.get("part_order") + if planner_order: + return list(planner_order) + unordered = constraints.get("part_identities") + if unordered: + return list(unordered) + return [] + + def _extract_warnings( + self, *, request: BuildRequest, constraints: Mapping[str, Any] + ) -> list[BuildWarning]: + warnings: list[BuildWarning] = [] + for item in constraints.get("ordering_warnings", []): + if isinstance(item, BuildWarning): + warnings.append(item) + elif isinstance(item, Mapping): + warnings.append( + BuildWarning( + code=str(item.get("code", "ordering_warning")), + message=str(item.get("message", "Ordering warning.")), + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=request.source_identity, + metadata=dict(item.get("metadata", {})), + ) + ) + return warnings + + def _infer_missing_kind(self, part_identity: str) -> str: + text = part_identity.lower() + for role in ("promoter", "rbs", "cds", "terminator"): + if role in text: + return role + return "reagent" diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py new file mode 100644 index 0000000..cdc0050 --- /dev/null +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -0,0 +1,277 @@ +"""Thin lvl2 assembly stage orchestration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import sbol2 + +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.inventory import CompatibilitySelector, Inventory +from buildcompiler.inventory.compatibility import Lvl2Route +from buildcompiler.sbol import AssemblyJob, AssemblyService + + +class AssemblyLvl2Stage: + def __init__( + self, + *, + inventory: Inventory, + selector: CompatibilitySelector | None = None, + assembly_service: AssemblyService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + self.selector = selector or CompatibilitySelector( + inventory, options=self.options + ) + self.assembly_service = assembly_service or AssemblyService() + + def run( + self, + request: BuildRequest, + *, + source_document: sbol2.Document, + target_document: sbol2.Document, + ) -> StageResult: + constraints = request.constraints or {} + module_definition = source_document.find(request.source_identity) + if not isinstance(module_definition, sbol2.ModuleDefinition): + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=[ + f"Source identity is not a ModuleDefinition: {request.source_identity}" + ], + ) + + region_identities = self._extract_region_identities( + module_definition, constraints + ) + if not region_identities: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=["No engineered-region identities found for lvl2 assembly."], + ) + + route_selection = self.selector.select_lvl2_route( + request_id=request.id, + region_identities=region_identities, + constraints=constraints, + ) + warning_logs: list[str] = [] + if route_selection.selected is None and constraints.get("region_order"): + relaxed_constraints = { + key: value for key, value in constraints.items() if key != "region_order" + } + relaxed_selection = self.selector.select_lvl2_route( + request_id=request.id, + region_identities=region_identities, + constraints=relaxed_constraints, + ) + if relaxed_selection.selected is not None: + route_selection = relaxed_selection + warning_logs.append( + "Unable to satisfy region_order constraint; proceeding with an arbitrary compatible order." + ) + + route = route_selection.selected + artifacts = self._route_artifacts(route, route_selection.rejected) + if route is None: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + protocol_artifacts=artifacts, + logs=[ + "No lvl2 route selected by CompatibilitySelector. Provide explicit region_order " + "or enable large-order search for large designs.", + *warning_logs, + ], + ) + + missing_inputs: list[MissingBuildInput] = [] + for missing_identity in route.missing_region_identities: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=missing_identity, + missing_display_id=missing_identity.rsplit("/", 1)[-1], + missing_kind="engineered_region", + required_stage=BuildStage.ASSEMBLY_LVL1, + reason="No compatible lvl1 engineered-region plasmid found in inventory.", + ) + ) + + if route.backbone is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity="backbone", + missing_display_id=None, + missing_kind="backbone", + required_stage="fatal", + reason="No compatible lvl2 backbone found in inventory.", + ) + ) + + restriction_enzyme_name = self.options.reagents.default_restriction_enzyme + restriction_enzyme = self.inventory.find_restriction_enzyme( + restriction_enzyme_name + ) + if restriction_enzyme is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=restriction_enzyme_name, + missing_display_id=restriction_enzyme_name, + missing_kind="restriction_enzyme", + required_stage="fatal", + reason="Required restriction enzyme is missing from inventory.", + ) + ) + + ligase_name = self.options.reagents.default_ligase + ligase = self.inventory.find_ligase(ligase_name) + if ligase is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=ligase_name, + missing_display_id=ligase_name, + missing_kind="ligase", + required_stage="fatal", + reason="Required ligase is missing from inventory.", + ) + ) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + protocol_artifacts=artifacts, + logs=[ + *warning_logs, + f"Blocked lvl2 assembly for {request.id}; missing {len(missing_inputs)} required input(s).", + ], + ) + + product_identity = ( + constraints.get("product_identity") or request.source_identity + ) + product_display_id = ( + constraints.get("product_display_id") + or request.source_display_id + or product_identity.rsplit("/", 1)[-1] + ) + assembly_result = self.assembly_service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL2, + product_identity=product_identity, + product_display_id=product_display_id, + part_plasmids=list(route.selected_lvl1_plasmids), + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ligase=ligase, + source_document=source_document, + target_document=target_document, + ) + ) + + for product in assembly_result.products: + insert_identities = list(product.metadata.get("insert_identities", [])) + if request.source_identity not in insert_identities: + insert_identities.append(request.source_identity) + product.metadata["insert_identities"] = insert_identities + product.metadata.setdefault("source_stage", BuildStage.ASSEMBLY_LVL2.value) + self.inventory.add_generated_product(product) + + json_intermediate = assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=route.selected_lvl1_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ) + + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=assembly_result.products, + sbol_document=assembly_result.stage_document, + json_intermediate=json_intermediate, + protocol_artifacts=artifacts, + logs=[ + *warning_logs, + f"Selected lvl2 route with {len(route.selected_lvl1_plasmids)} lvl1 plasmid(s).", + *assembly_result.logs, + ], + ) + + def _extract_region_identities( + self, module_definition: sbol2.ModuleDefinition, constraints: Mapping[str, Any] + ) -> list[str]: + for key in ("engineered_region_identities", "region_identities"): + values = constraints.get(key) + if values: + return list(values) + + identities: list[str] = [] + for functional_component in module_definition.functionalComponents: + definition = functional_component.definition + if definition: + identities.append(definition) + return identities + + def _route_artifacts( + self, selected: Lvl2Route | None, rejected: tuple[Any, ...] + ) -> dict[str, Any]: + return { + "selected_route": self._route_to_dict(selected), + "rejected_routes": [self._route_to_dict(route) for route in rejected[:3]], + } + + def _route_to_dict(self, route: Lvl2Route | None) -> dict[str, Any] | None: + if route is None: + return None + return { + "region_order": list(route.region_order), + "selected_lvl1_plasmids": [ + p.identity for p in route.selected_lvl1_plasmids + ], + "missing_region_identities": list(route.missing_region_identities), + "score": { + "missing_required_products": route.score.missing_required_products, + "missing_domestications": route.score.missing_domestications, + "missing_lvl1_plasmids": route.score.missing_lvl1_plasmids, + "generated_or_planned_materials": route.score.generated_or_planned_materials, + "lower_material_state_penalty": route.score.lower_material_state_penalty, + "constraint_violations": route.score.constraint_violations, + "total_assemblies": route.score.total_assemblies, + "identity_tiebreak": list(route.score.identity_tiebreak), + }, + } diff --git a/src/buildcompiler/stages/domestication.py b/src/buildcompiler/stages/domestication.py new file mode 100644 index 0000000..5e729ba --- /dev/null +++ b/src/buildcompiler/stages/domestication.py @@ -0,0 +1,149 @@ +"""Domestication stage orchestration.""" + +from __future__ import annotations + +import sbol2 + +from buildcompiler.api.options import BuildOptions, ProtocolMode +from buildcompiler.domain import ( + ApprovalStatus, + BuildRequest, + BuildStage, + BuildWarning, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.planning.domestication import DomesticationPlanner +from buildcompiler.sbol.domestication import DomesticationJob, DomesticationService + + +class DomesticationStage: + def __init__( + self, + *, + inventory: Inventory, + domestication_planner: DomesticationPlanner | None = None, + domestication_service: DomesticationService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.domestication_planner = domestication_planner or DomesticationPlanner() + self.domestication_service = domestication_service or DomesticationService() + self.options = options or BuildOptions() + + def run(self, request: BuildRequest, *, source_document: sbol2.Document, target_document: sbol2.Document) -> StageResult: + part_component = source_document.find(request.source_identity) + if not isinstance(part_component, sbol2.ComponentDefinition): + for candidate in source_document.componentDefinitions: + if ( + candidate.identity == request.source_identity + or candidate.persistentIdentity == request.source_identity + or candidate.displayId == request.source_identity + or candidate.identity.endswith(f"/{request.source_identity}/1") + or candidate.persistentIdentity.endswith(f"/{request.source_identity}") + ): + part_component = candidate + break + if not isinstance(part_component, sbol2.ComponentDefinition): + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=[f"Failed domestication: source part {request.source_identity} not found."], + ) + try: + plan = self.domestication_planner.plan(part_component) + except ValueError as exc: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.FAILED, + request_ids=[request.id], + warnings=[BuildWarning(code="domestication.invalid_input", message=str(exc), stage=BuildStage.DOMESTICATION, source_identity=request.source_identity)], + logs=[str(exc)], + ) + + missing_inputs: list[MissingBuildInput] = [] + backbone = self.inventory.find_backbone(stage=BuildStage.DOMESTICATION) + if backbone is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, "backbone", None, "backbone", "fatal", "No domestication backbone found in inventory.")) + + restriction = self.inventory.find_restriction_enzyme(self.options.reagents.default_restriction_enzyme) + if restriction is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, self.options.reagents.default_restriction_enzyme, self.options.reagents.default_restriction_enzyme, "restriction_enzyme", "fatal", "Required domestication restriction enzyme missing from inventory.")) + + ligase = self.inventory.find_ligase(self.options.reagents.default_ligase) + if ligase is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, self.options.reagents.default_ligase, self.options.reagents.default_ligase, "ligase", "fatal", "Required domestication ligase missing from inventory.")) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + logs=["Domestication blocked on missing backbone/reagents."], + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals]}, + ) + + approvals: list[RequiredApproval] = [] + if plan.sequence_edit_proposals: + approval_id = f"domestication-edit:{request.source_identity}" + process_approved = "domestication_sequence_edit" in self.options.approvals.approved_processes + id_approved = approval_id in self.options.approvals.approved_approval_ids + allow_edits = self.options.domestication.allow_sequence_domestication_edits + protocol_mode_active = self.options.protocol.mode != ProtocolMode.NONE + if (not allow_edits) or (protocol_mode_active and not (process_approved or id_approved)): + approvals.append( + RequiredApproval( + status=ApprovalStatus.REQUIRED, + process="domestication_sequence_edit", + reason="Sequence edits were proposed and require explicit human approval.", + metadata={ + "approval_id": approval_id, + "part_identity": request.source_identity, + "proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals], + }, + ) + ) + + if approvals: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[request.id], + required_approvals=approvals, + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals]}, + logs=["Domestication blocked pending sequence-edit approval."], + ) + + result = self.domestication_service.run( + DomesticationJob( + part_identity=request.source_identity, + part_display_id=request.source_display_id, + part_component=part_component, + backbone=backbone, + restriction_enzyme=restriction, + ligase=ligase, + source_document=source_document, + target_document=target_document, + sequence_edit_proposals=plan.sequence_edit_proposals, + ) + ) + self.inventory.add_generated_product(result.product) + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[result.product], + sbol_document=result.stage_document, + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals], **result.artifacts}, + logs=result.logs, + ) diff --git a/src/sbol2build/sbol2build.py b/src/sbol2build/sbol2build.py index db6a755..169804e 100644 --- a/src/sbol2build/sbol2build.py +++ b/src/sbol2build/sbol2build.py @@ -6,7 +6,7 @@ from typing import List, Union, Tuple from .constants import DNA_TYPES -sbol2.Config.setHomespace("https://SBOL2Build.org") +sbol2.Config.setHomespace("http://buildcompiler.org") sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True) sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False) @@ -1010,7 +1010,6 @@ def append_extracts_to_doc( """ for extract, sequence in extract_tuples: try: - print("adding: " + extract.displayId) add_object_to_doc(extract, doc) add_object_to_doc(sequence, doc) except Exception as e: diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b48a94d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,33 @@ +# Testing guide + +## Core (required) checks + +Core CI is compiler-only and offline (except fixture-local SBOL files). + +```bash +ruff check . +ruff format --check . +pytest tests/unit tests/stages tests/integration +``` + +Core tests must not require SynBioHub, PUDU, Opentrons, or SBOLInventory. + +## Optional automation checks + +```bash +python -m pip install -e '.[automation,test]' +pytest tests/automation +``` + +Automation tests are marked with `@pytest.mark.automation` and are manual/optional. + +## Skip vs xfail guidance + +- Use `skip` for intentionally manual checks (e.g., hardware simulation). +- Use `xfail` only for known core blockers and include explicit issue references. + +## Fixtures + +- `tests/fixtures/sbol/`: fixture-local SBOL files. +- `tests/fixtures/data/`: small deterministic JSON/data fixtures. +- Shared fixture helpers are in `tests/conftest.py`. diff --git a/tests/automation/README.md b/tests/automation/README.md new file mode 100644 index 0000000..b083daf --- /dev/null +++ b/tests/automation/README.md @@ -0,0 +1,10 @@ +# Automation test suite (optional/manual) + +These tests are intentionally separated from core CI. + +Run only when optional dependencies are installed: + +```bash +python -m pip install -e '.[automation,test]' +pytest tests/automation +``` diff --git a/tests/automation/test_opentrons_simulation_optional.py b/tests/automation/test_opentrons_simulation_optional.py new file mode 100644 index 0000000..014ddec --- /dev/null +++ b/tests/automation/test_opentrons_simulation_optional.py @@ -0,0 +1,7 @@ +import pytest + +pytestmark = pytest.mark.automation + + +def test_opentrons_simulation_manual_only(): + pytest.skip("Manual/optional automation validation only. TODO(#67): wire real simulation checks in automation environment.") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..18e616f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sbol2 +import pytest + +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildRequest, BuildStage, DesignKind + + +@pytest.fixture +def default_build_options() -> BuildOptions: + options = BuildOptions() + options.execution.max_iterations = 5 + return options + + +@pytest.fixture +def minimal_sbol_document() -> sbol2.Document: + return sbol2.Document() + + +@pytest.fixture +def minimal_lvl2_request() -> BuildRequest: + return BuildRequest( + id="req-lvl2-1", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://example.org/module/main", + source_display_id="main", + source_kind=DesignKind.MODULE_DEFINITION, + ) diff --git a/tests/integration/test_full_build_happy_path.py b/tests/integration/test_full_build_happy_path.py new file mode 100644 index 0000000..2dddbff --- /dev/null +++ b/tests/integration/test_full_build_happy_path.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import sys + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.planning import BuildPlan +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, result_factory): + self.result_factory = result_factory + + def run(self, request, *, source_document, target_document): + return self.result_factory(request) + + +def _product(identity: str) -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=MaterialState.GENERATED, + ) + + +def test_full_build_happy_path_offline(default_build_options, minimal_sbol_document): + lvl2_request = BuildRequest( + id="req-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://example.org/module/target", + source_display_id="target", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ctx = BuildContext( + sbol=SbolResolver(minimal_sbol_document), + inventory=Inventory(), + build_document=minimal_sbol_document, + options=default_build_options, + ) + executor = FullBuildExecutor( + context=ctx, + lvl2_stage=FakeStage( + lambda request: StageResult( + id="res-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[_product("https://example.org/plasmid/lvl2_target")], + ) + ), + lvl1_stage=FakeStage(lambda request: StageResult(id="unused1", stage=request.stage, status=StageStatus.BLOCKED, request_ids=[request.id])), + domestication_stage=FakeStage(lambda request: StageResult(id="unused2", stage=request.stage, status=StageStatus.BLOCKED, request_ids=[request.id])), + ) + + result = executor.execute(BuildPlan(lvl2_requests=[lvl2_request])) + + assert result.status == BuildStatus.SUCCESS + assert result.summary is not None + assert result.final_products + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules diff --git a/tests/integration/test_missing_lvl1_then_domestication.py b/tests/integration/test_missing_lvl1_then_domestication.py new file mode 100644 index 0000000..a0a91a6 --- /dev/null +++ b/tests/integration/test_missing_lvl1_then_domestication.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.planning import BuildPlan +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, fn): + self.fn = fn + + def run(self, request, *, source_document, target_document): + return self.fn(request) + + +def plasmid(identity: str) -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=MaterialState.GENERATED, + ) + + +def test_missing_lvl1_promotes_domestication_and_retries( + default_build_options, minimal_sbol_document +): + lvl1_attempts = {"n": 0} + + def lvl1_fn(request): + lvl1_attempts["n"] += 1 + if lvl1_attempts["n"] == 1: + return StageResult( + id="lvl1-blocked", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=[ + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity="https://example.org/part/promoterA", + missing_display_id="promoterA", + missing_kind="promoter", + required_stage=BuildStage.DOMESTICATION, + reason="missing part", + ) + ], + ) + return StageResult( + id="lvl1-success", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://example.org/plasmid/lvl1_after_dom")], + ) + + def domestication_fn(request): + return StageResult( + id="dom-success", + stage=BuildStage.DOMESTICATION, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://example.org/plasmid/dom_promoterA")], + ) + + context = BuildContext( + sbol=SbolResolver(minimal_sbol_document), + inventory=Inventory(), + build_document=minimal_sbol_document, + options=default_build_options, + ) + executor = FullBuildExecutor( + context=context, + lvl2_stage=FakeStage( + lambda request: StageResult( + id="unused-lvl2", + stage=request.stage, + status=StageStatus.BLOCKED, + request_ids=[request.id], + ) + ), + lvl1_stage=FakeStage(lvl1_fn), + domestication_stage=FakeStage(domestication_fn), + ) + + plan = BuildPlan( + lvl1_requests=[ + BuildRequest( + id="req-lvl1", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity="https://example.org/engineered/region1", + source_display_id="region1", + source_kind=DesignKind.COMPONENT_DEFINITION, + ) + ] + ) + + result = executor.execute(plan) + + assert lvl1_attempts["n"] >= 2 + assert any(sr.stage == BuildStage.DOMESTICATION for sr in result.stage_results) + assert any(p.display_id == "dom_promoterA" for p in result.final_products) + assert result.status in {BuildStatus.SUCCESS, BuildStatus.PARTIAL_SUCCESS} diff --git a/tests/stages/test_domestication.py b/tests/stages/test_domestication.py new file mode 100644 index 0000000..75394d1 --- /dev/null +++ b/tests/stages/test_domestication.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import DomesticationStage + + +def test_domestication_stage_importable(): + assert DomesticationStage diff --git a/tests/stages/test_stage_assembly_lvl1.py b/tests/stages/test_stage_assembly_lvl1.py new file mode 100644 index 0000000..df4b638 --- /dev/null +++ b/tests/stages/test_stage_assembly_lvl1.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import AssemblyLvl1Stage + + +def test_assembly_lvl1_stage_importable(): + assert AssemblyLvl1Stage diff --git a/tests/stages/test_stage_assembly_lvl2.py b/tests/stages/test_stage_assembly_lvl2.py new file mode 100644 index 0000000..e2fd0eb --- /dev/null +++ b/tests/stages/test_stage_assembly_lvl2.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import AssemblyLvl2Stage + + +def test_assembly_lvl2_stage_importable(): + assert AssemblyLvl2Stage diff --git a/tests/test_abstract.py b/tests/test_abstract.py deleted file mode 100644 index 00ba486..0000000 --- a/tests/test_abstract.py +++ /dev/null @@ -1,146 +0,0 @@ -# test 1: test same abstract design with each possible circuit selection, ensure the promoter and terminator shift accordingly - -# test 2: inaccessible part in abstract design -> should throw informative error message - -# test 3: (FUTURE) abstract design with multiple TUs - -import sbol2 -import unittest -import sys -import os - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from sbol2build.abstract_translator import ( - translate_abstract_to_plasmids, - copy_sequences, -) - -from sbol2build import ( - golden_gate_assembly_plan, -) - - -class Test_Abstract_Translation_Functions(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.sbh = sbol2.PartShop("https://synbiohub.org") - - cls.plasmid_collection = sbol2.Document() - cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1/b7fdc21c6601a61d3166073a9e50f2c3843e1df5/share", - cls.plasmid_collection, - ) - - cls.DVK_AE_doc = sbol2.Document() - cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1/647c5b2458567dcce6b0a37178d352b8ffa9a7fe/share", - cls.DVK_AE_doc, - ) - - def test_simple_abstract_translation(self): - abstract_design_doc = sbol2.Document() - abstract_design_doc.read("tests/test_files/moclo_parts_circuit.xml") - - mocloplasmid_list = translate_abstract_to_plasmids( - abstract_design_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(mocloplasmid_list), - 4, - "There should be 4 plasmids in the abstract translation", - ) - - prev_site = "A" - for plas in mocloplasmid_list: - self.assertEqual(prev_site, plas.fusion_sites[0], mocloplasmid_list) - prev_site = plas.fusion_sites[1] - - def test_two_rbs_combinatorial_translation(self): - comb_doc = sbol2.Document() - comb_doc.read("tests/test_files/combinatorial_1.xml") - - comb_plasmid_list = translate_abstract_to_plasmids( - comb_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(comb_plasmid_list), - 5, - "There should be 5 plasmids in the abstract translation", - ) - - # Run through sbol2build to test composite count - part_documents = [] - - for mocloPlasmid in comb_plasmid_list: - temp_doc = sbol2.Document() - mocloPlasmid.definition.copy(temp_doc) - copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - part_documents.append(temp_doc) - - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "combinatorial_rbs_assembly_plan", - part_documents, - self.DVK_AE_doc, - "BsaI", - assembly_doc, - ) - - composite_list = assembly_obj.run() - assembly_doc.write("comb_assembly.xml") - - self.assertEqual( - len(composite_list), - 2, - "Combinatorial assembly failed to produce 2 composites", - ) - - def test_complex_combinatorial_translation( - self, - ): # testing combinatorial design with 3 variable promoters and RBSs - complex_comb_doc = sbol2.Document() - complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") - - comb_plasmid_list = translate_abstract_to_plasmids( - complex_comb_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(comb_plasmid_list), - 8, - f"There should be 8 plasmids in the abstract translation, found {len(comb_plasmid_list)}", - ) - - # Run through sbol2build to test composite count - part_documents = [] - - for mocloPlasmid in comb_plasmid_list: - temp_doc = sbol2.Document() - mocloPlasmid.definition.copy(temp_doc) - copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - part_documents.append(temp_doc) - - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "complex_combinatorial_assembly_plan", - part_documents, - self.DVK_AE_doc, - "BsaI", - assembly_doc, - ) - - composite_list = assembly_obj.run() - assembly_doc.write("complex_comb_assembly.xml") - - self.assertEqual( - len(composite_list), - 9, - f"Combinatorial assembly failed to produce 9 composites, found {len(composite_list)}", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py new file mode 100644 index 0000000..4075e2e --- /dev/null +++ b/tests/test_buildcompiler.py @@ -0,0 +1,682 @@ +import sbol2 +import unittest +import sys +import os +import copy +from collections import Counter + + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler, _extract_lvl2_TUs + +from buildcompiler.abstract_translator import extract_toplevel_definition, get_or_pull + + +class Test_Buildcompiler_Functions(unittest.TestCase): + @classmethod + def setUpClass(cls): + username = os.environ.get("SBH_USERNAME") + password = os.environ.get("SBH_PASSWORD") + + if not username or not password: + raise RuntimeError( + "Missing SBH_USERNAME and/or SBH_PASSWORD environment variables" + ) + sbh = sbol2.PartShop("https://api.synbiohub.org") + sbh.login(username, password) + + auth = sbh.key + + collections = [ + "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + ] + + source = sbol2.Document() + + # preload combinatorial designs into buildcompiler context + source.read("tests/test_files/complex_combinatorial_abstract.xml") + source.append("tests/test_files/combinatorial_1.xml", True) + + cls.buildcompiler = BuildCompiler( + collections, "https://api.synbiohub.org", auth, source, server_mode=True + ) + + def test_simple_lvl1_assembly(self): + abstract_design_doc = sbol2.Document() + abstract_design_doc.read("tests/test_files/moclo_parts_circuit.xml") + + design = extract_toplevel_definition(abstract_design_doc) + product_doc = sbol2.Document() + + dict, product_doc = self.buildcompiler.assembly_lvl1([design], product_doc) + + self.assertEqual( + len(dict), + 1, + "There should be 1 composite resulting from the assembly", + ) + + assembly_activity = product_doc.get( + "http://buildcompiler.org/qlSBuNBL_composite_assembly/1" + ) + + self.assertEqual( + len(assembly_activity.usages), + 7, + "Assembly should have 7 usages: 5 plasmids, 1 ligase, 1 Restriction Enzyme", + ) + + usage_uris = {u.identity for u in assembly_activity.usages} + + expected_usage_uris = { + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pJ23100_AB_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pB0034_BC_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pE0030_CD_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pB0015_DE_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/DVK_AE_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/BsaI_enzyme/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/T4_Ligase/1", + } + + for expected_uri in expected_usage_uris: + self.assertIn( + expected_uri, + usage_uris, + f"Expected usage {expected_uri} was not found in assembly activity usages", + ) + + usage = next( + u for u in assembly_activity.usages if str(u.identity) == expected_uri + ) + + impl = get_or_pull(product_doc, self.buildcompiler.sbh, usage.entity, True) + + self.assertIsNotNone( + impl, + f"Entity {usage.entity} should exist in the document or on SynBioHub", + ) + + self.assertIsInstance( + impl, + sbol2.Implementation, + f"Entity {usage.entity} should be an SBOL Implementation", + ) + + self.assertIsNotNone( + impl.built, + f"Implementation {impl.identity} should reference a built object", + ) + + expected_product_uri = "http://buildcompiler.org/qlSBuNBL_composite_impl/1" + + product_impl = product_doc.get(expected_product_uri) + product_def = product_doc.get(product_impl.built) + + self.assertIsNotNone( + product_impl, + f"Implementation {product_impl} should exist in the document", + ) + + self.assertEqual( + product_impl.wasGeneratedBy, + [assembly_activity.identity], + f"Implementation {product_impl} must include wasGeneratedBy {assembly_activity.identity}", + ) + + self.assertEqual( + product_def.identity, + "http://buildcompiler.org/qlSBuNBL_composite/1", + f"Implementation {product_impl} must build {product_def}", + ) + + def test_two_rbs_combinatorial_translation(self): + comb_doc = sbol2.Document() + comb_doc.read("tests/test_files/combinatorial_1.xml") + + design = comb_doc.combinatorialderivations[0] + + result_dict, assembly_doc = self.buildcompiler.assembly_lvl1(design) + + self.assertEqual( + len(result_dict), + 1, + "Expected one combinatorial derivation key in result dictionary", + ) + + derivation_uri = "https://sbolcanvas.org/abstract_combinatorial/1" + + self.assertIn( + derivation_uri, + result_dict, + "Expected combinatorial derivation URI missing from results", + ) + + composites = result_dict[derivation_uri] + + self.assertEqual( + len(composites), + 2, + "Combinatorial assembly failed to produce 2 composites", + ) + + # ensure cobinatorial feature is satsified + for composite in composites: + components = composite.plasmid_definition.getInSequentialOrder() + + if len(components) > 3: + if components[3].displayId == "B0033": + has_b0033 = True + elif components[3].displayId == "B0032": + has_b0032 = True + + self.assertTrue(has_b0033, "No composite has B0033") + self.assertTrue(has_b0032, "No composite has B0032") + + def test_complex_combinatorial_translation( + self, + ): # testing combinatorial design with 3 variable promoters and RBSs + complex_comb_doc = sbol2.Document() + complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") + + design = complex_comb_doc.combinatorialderivations[0] + + result_dict, assembly_doc = self.buildcompiler.assembly_lvl1(design) + + assembly_doc.write("comb_assembly.xml") + + self.assertEqual( + len(result_dict), + 1, + "Expected one combinatorial derivation key in result dictionary", + ) + + derivation_uri = "https://sbolcanvas.org/dEOuAjnj/1" + + self.assertIn( + derivation_uri, + result_dict, + "Expected combinatorial derivation URI missing from results", + ) + + composites = result_dict[derivation_uri] + + self.assertEqual( + len(composites), + 9, + f"Combinatorial assembly failed to produce 9 composites, found {len(composites)}", + ) + + promoter_counts = Counter() + rbs_counts = Counter() + + for composite in composites: + components = composite.plasmid_definition.getInSequentialOrder() + display_ids = [component.displayId for component in components] + print(display_ids) + + promoter_counts[components[1].displayId] += 1 # index 1 = promoter + rbs_counts[components[3].displayId] += 1 # index 3 = RBS + + self.assertEqual( + promoter_counts["J23100"], + 3, + f"Expected J23100 to appear 3 times across the composite dictionary, found {promoter_counts['J23100']}", + ) + + self.assertEqual( + promoter_counts["J23106"], + 3, + f"Expected J23106 to appear 3 times across the composite dictionary, found {promoter_counts['J23106']}", + ) + + self.assertEqual( + promoter_counts["J23116"], + 3, + f"Expected J23116 to appear 3 times across the composite dictionary, found {promoter_counts['J23116']}", + ) + + self.assertEqual( + rbs_counts["B0034"], + 3, + f"Expected B0034 to appear 3 times across the composite dictionary, found {rbs_counts['B0034']}", + ) + + self.assertEqual( + rbs_counts["B0032"], + 3, + f"Expected B0032 to appear 3 times across the composite dictionary, found {rbs_counts['B0032']}", + ) + + self.assertEqual( + rbs_counts["B0033"], + 3, + f"Expected B0033 to appear 3 times across the composite dictionary, found {rbs_counts['B0033']}", + ) + + def test_simple_lvl2_assembly(self): + """ + High-level integration test for lvl2 assembly. + + Validates: + - lvl2 plasmid generation succeeds + - lvl1 intermediates are used as assembly inputs + - correct enzymes are included + - correct backbone selection occurs + - TU ordering is preserved + - SBOL provenance relationships are intact + """ + + abstract_design_doc = sbol2.Document() + abstract_design_doc.read("tests/test_files/ExampleLvl2_design.xml") + + design_TUs = _extract_lvl2_TUs(abstract_design_doc) + + # ------------------------------------------------------------ + # Run lvl2 assembly + # ------------------------------------------------------------ + lvl2_plasmids, final_doc = self.buildcompiler.assembly_lvl2( + abstract_design_doc, + product_name="lvl2", + ) + + self.assertEqual( + len(lvl2_plasmids), + 1, + "Expected exactly one lvl2 plasmid to be produced", + ) + + lvl2_plasmid = lvl2_plasmids[0] + + # ------------------------------------------------------------ + # Validate provenance / implementation relationships + # ------------------------------------------------------------ + product_impl = lvl2_plasmid.plasmid_implementations[0] + product_def = lvl2_plasmid.plasmid_definition + + self.assertIsNotNone( + product_impl, + "Lvl2 plasmid implementation should exist", + ) + + self.assertIsNotNone( + product_def, + "Lvl2 plasmid definition should exist", + ) + + self.assertEqual( + product_impl.built, + product_def.identity, + "Implementation should reference the built lvl2 construct", + ) + + # test level 1 assembly activities + lvl1_activities = [ + "http://buildcompiler.org/Gen_Gen1_plas_assembly/1", + "http://buildcompiler.org/Gen1_Gen1_plas_assembly/1", + ] + + for index, activity_id in enumerate(lvl1_activities): + lvl1_activity = final_doc.get(activity_id) + + self.assertIsNotNone( + lvl1_activity, + f"Lvl1 assembly activity {activity_id} should exist", + ) + + # ------------------------------------------------------------ + # Validate lvl1 activity usages + # ------------------------------------------------------------ + lvl1_usage_entities = [ + get_or_pull(final_doc, self.buildcompiler.sbh, u.entity, True) + for u in lvl1_activity.usages + ] + + lvl1_usage_display_ids = { + entity.displayId for entity in lvl1_usage_entities if entity is not None + } + + lvl1_usage_built_CDs = { + final_doc.get(entity.built) + for entity in lvl1_usage_entities + if entity is not None + } + + for comp in design_TUs[index].components: + self.assertIn( + comp.definition, + [ + subcomp.definition + for plas in lvl1_usage_built_CDs + for subcomp in plas.components + ], + f"Level 1 assembly activity {activity_id} missing {comp.definition} from design TU {design_TUs[index]}", + ) + + self.assertIn( + "BsaI_impl", + lvl1_usage_display_ids, + "Lvl1 assembly should use BsaI", + ) + + self.assertIn( + "T4_Ligase_impl", + lvl1_usage_display_ids, + "Lvl1 assembly should use T4 ligase", + ) + + for entity in lvl1_usage_entities: + self.assertIsNotNone( + entity, + "All lvl1 assembly usage entities should resolve correctly", + ) + + self.assertIsInstance( + entity, + sbol2.Implementation, + "Lvl1 assembly usages should reference SBOL implementations", + ) + + self.assertGreaterEqual( + len(lvl1_usage_entities), + 3, + "Lvl1 assembly should contain enzyme and plasmid usages", + ) + + # test level2 assembly activity + lvl2_assembly_activity = final_doc.get( + "http://buildcompiler.org/lvl2_assembly/1" + ) + + self.assertIsNotNone( + lvl2_assembly_activity, + "Assembly activity should exist in final document", + ) + + self.assertEqual( + product_impl.wasGeneratedBy, + [lvl2_assembly_activity.identity], + "Lvl2 implementation should reference generating assembly activity", + ) + + # ------------------------------------------------------------ + # Validate expected assembly usages + # ------------------------------------------------------------ + usage_entities = [ + get_or_pull(final_doc, self.buildcompiler.sbh, u.entity, True) + for u in lvl2_assembly_activity.usages + ] + + usage_display_ids = { + entity.displayId for entity in usage_entities if entity is not None + } + + self.assertIn( + "BbsI_impl", + usage_display_ids, + "Lvl2 assembly should use BbsI", + ) + + self.assertIn( + "T4_Ligase_impl", + usage_display_ids, + "Lvl2 assembly should use T4 ligase", + ) + + # ------------------------------------------------------------ + # Ensure lvl1 plasmids were used as inputs + # ------------------------------------------------------------ + lvl1_inputs = [ + entity + for entity in usage_entities + if entity is not None and "_plas" in entity.displayId.lower() + ] + + self.assertEqual( + len(lvl1_inputs), + 2, + "Lvl2 assembly should consume 2 lvl1 plasmids as inputs", + ) + + # ------------------------------------------------------------ + # Validate final construct ordering + # ------------------------------------------------------------ + components = product_def.getInSequentialOrder() + display_ids = [component.displayId for component in components] + + expected_order = [ + "Ligation_Scar_A", + "Gen_Gen1_plas_TU", + "Ligation_Scar_E", + "Gen1_Gen1_plas_TU", + "Ligation_Scar_F", + "dva_backbone_core", + ] + + self.assertEqual( + display_ids, + expected_order, + "Lvl2 construct does not preserve expected TU ordering", + ) + + # ------------------------------------------------------------ + # Validate all usage entities are valid implementations + # ------------------------------------------------------------ + for entity in usage_entities: + self.assertIsNotNone( + entity, + "All assembly usage entities should resolve correctly", + ) + + self.assertIsInstance( + entity, + sbol2.Implementation, + "Assembly usages should reference SBOL implementations", + ) + + self.assertIsNotNone( + entity.built, + f"Implementation {entity.identity} should reference a built object", + ) + + # Ensure no duplicate lvl1 intermediates were generated + lvl1_ids = [entity.displayId for entity in lvl1_inputs] + + self.assertEqual( + len(lvl1_ids), + len(set(lvl1_ids)), + "Duplicate lvl1 intermediates detected", + ) + + def test_transformation(self): + transformation_doc = sbol2.Document() + + chassis_md = sbol2.ModuleDefinition("E_coli_DH5alpha") + chassis_impl = sbol2.Implementation("E_coli_DH5alpha_impl") + chassis_impl.built = chassis_md.identity + + transformation_doc.add(chassis_md) + transformation_doc.add(chassis_impl) + + result = self.buildcompiler.transformation( + assembly_products=self.buildcompiler.indexed_plasmids[:2], + chassis_name="E_coli_DH5alpha", + transformation_doc=transformation_doc, + ) + + # Top-level output structure + self.assertEqual(result["stage"], "transformation") + + plasmid_displayIds = [ + plasmid.plasmid_definition.displayId + for plasmid in self.buildcompiler.indexed_plasmids[:2] + ] + + self.assertEqual( + result["inputs"], + plasmid_displayIds, + ) + + self.assertEqual( + result["chassis"], + "E_coli_DH5alpha", + ) + + self.assertIn("sbol_artifacts", result) + self.assertIn("json_intermediate", result) + self.assertIn("protocol_artifacts", result) + + # Robot JSON intermediate + json_intermediate = result["json_intermediate"] + + self.assertEqual( + json_intermediate["protocol"], + "chemical_transformation", + ) + + self.assertEqual( + json_intermediate["version"], + "0.1", + ) + + self.assertEqual(len(json_intermediate["steps"]), 2) + + first_step = json_intermediate["steps"][0] + + self.assertEqual(first_step["step"], 1) + + self.assertEqual(first_step["plasmid"], plasmid_displayIds[0]) + + self.assertEqual( + first_step["heat_shock"], + { + "temperature_c": 42, + "duration_seconds": 45, + }, + ) + + # SBOL artifact outputs + sbol_artifacts = result["sbol_artifacts"] + + self.assertEqual(len(sbol_artifacts), 2) + + first_artifact = sbol_artifacts[0] + + self.assertIn("transformation_activity", first_artifact) + + self.assertIn( + "transformed_strain_module", + first_artifact, + ) + + self.assertIn( + "transformed_strain_implementation", + first_artifact, + ) + + # Verify generated SBOL objects exist in document + transform_activity = transformation_doc.get( + f"http://buildcompiler.org/transform_{plasmid_displayIds[0]}_1/1" + ) + + self.assertIsInstance( + transform_activity, + sbol2.Activity, + ) + + self.assertEqual( + len(transform_activity.usages), + 2, + ) + + self.assertEqual( + len(transform_activity.associations), + 1, + ) + + association = transform_activity.associations[0] + + self.assertIsNotNone(association.plan) + + self.assertIsNotNone(association.agent) + + # Verify transformed strain + transformed_strain = transformation_doc.get( + f"http://buildcompiler.org/E_coli_DH5alpha_with_{plasmid_displayIds[0]}/1" + ) + + self.assertIsInstance( + transformed_strain, + sbol2.ModuleDefinition, + ) + + self.assertEqual( + len(transformed_strain.modules), + 1, + ) + + self.assertEqual( + len(transformed_strain.functionalComponents), + 1, + ) + + # Verify transformed implementation + transformed_impl = transformation_doc.get( + f"http://buildcompiler.org/E_coli_DH5alpha_with_{plasmid_displayIds[0]}_impl/1" + ) + + self.assertIsInstance( + transformed_impl, + sbol2.Implementation, + ) + + self.assertEqual( + transformed_impl.built, + transformed_strain.identity, + ) + + self.assertEqual( + transformed_impl.wasGeneratedBy, + [transform_activity.identity], + ) + + # Verify protocol artifacts/logging + protocol_artifacts = result["protocol_artifacts"] + + self.assertIn( + "ot2_script", + protocol_artifacts, + ) + + self.assertIn( + "human_instructions", + protocol_artifacts, + ) + + self.assertIn( + "logs", + protocol_artifacts, + ) + + self.assertEqual( + len(protocol_artifacts["logs"]), + 2, + ) + + # Error handling + invalid_plasmid = copy.deepcopy(self.buildcompiler.indexed_plasmids[4]) + + invalid_plasmid.plasmid_implementations = [] + + with self.assertRaises( + ValueError, msg="Plasmid object with no implementations should throw error" + ): + self.buildcompiler.transformation( + assembly_products=[invalid_plasmid], + transformation_doc=transformation_doc, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_buildcompiler_transformation.py b/tests/test_buildcompiler_transformation.py new file mode 100644 index 0000000..0b9a6e7 --- /dev/null +++ b/tests/test_buildcompiler_transformation.py @@ -0,0 +1,64 @@ +import os +import sys +import unittest + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID + + +class TestBuildCompilerTransformation(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + + def _make_plasmid(self, display_id: str) -> sbol2.ComponentDefinition: + plasmid = sbol2.ComponentDefinition(display_id) + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + return plasmid + + def test_transformation_accepts_component_definitions(self): + p1 = self._make_plasmid("geneA_plasmid") + p2 = self._make_plasmid("geneB_plasmid") + + result = self.compiler.transformation([p1, p2], chassis_name="DH5alpha") + + self.assertEqual(result["stage"], "transformation") + self.assertEqual(result["chassis"], "DH5alpha") + self.assertEqual(len(result["sbol_artifacts"]), 2) + self.assertEqual(len(result["json_intermediate"]["steps"]), 2) + self.assertEqual( + result["json_intermediate"]["steps"][0]["plasmid"], "geneA_plasmid" + ) + self.assertIn("logs", result["protocol_artifacts"]) + + def test_transformation_accepts_dict_payloads(self): + plasmid = self._make_plasmid("geneC_plasmid") + result = self.compiler.transformation( + [{"name": "lvl1_geneC_output", "plasmid": plasmid}] + ) + + self.assertEqual(result["inputs"], ["lvl1_geneC_output"]) + self.assertEqual( + result["sbol_artifacts"][0]["transformed_strain_module"].endswith( + "E_coli_DH5alpha_with_geneC_plasmid/1" + ), + True, + ) + + def test_transformation_requires_inputs(self): + with self.assertRaises(ValueError): + self.compiler.transformation([]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py index 8953a4d..868bd7a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,41 +5,119 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) -from sbol2build import ( - golden_gate_assembly_plan, - rebase_restriction_enzyme, +from buildcompiler.constants import ( + CIRCULAR, + ENGINEERED_INSERT, + ENGINEERED_PLASMID, + FIVE_PRIME_OVERHANG, + LINEAR, + PLASMID_VECTOR, + THREE_PRIME_OVERHANG, +) +from buildcompiler.sbol2build import ( + Assembly, backbone_digestion, part_digestion, ligation, ) +from buildcompiler.plasmid import Plasmid + + +class Test_Assembly_Functions(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.sbh = sbol2.PartShop("https://api.synbiohub.org") + + username = os.environ.get("SBH_USERNAME") + password = os.environ.get("SBH_PASSWORD") + + if not username or not password: + raise RuntimeError( + "Missing SBH_USERNAME and/or SBH_PASSWORD environment variables" + ) + + cls.sbh.login(username, password) + + cls.source_doc = sbol2.Document() + final_doc = sbol2.Document() + + cls.sbh.pull( + "https://api.synbiohub.org/user/Gon/CIDARMoCloParts/CIDARMoCloParts_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://api.synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + cls.source_doc, + ) + + cls.re_impl = cls.source_doc.get( + "https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" + ) + cls.ligase_impl = cls.source_doc.get( + "https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" + ) -class Test_Core_Functions(unittest.TestCase): - def test_part_digestion(self): - doc = sbol2.Document() - doc.read("tests/test_files/pro_in_bb.xml") + cls.assembly = Assembly( + None, None, cls.re_impl, cls.ligase_impl, cls.source_doc, final_doc + ) - md = doc.getModuleDefinition("https://sbolcanvas.org/module1") - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + def test_part_digestion(self): # TODO test activity relationships + impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + ) + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + assembly_activity = self.assembly.initialize_assembly_activity() - parts_list, assembly_plan = part_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, doc + parts_list, assembly_activity = part_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) product_doc = sbol2.Document() - for extract, sequence in parts_list: + for extract, _ in parts_list: product_doc.add(extract) - product_doc.add(sequence) - product_doc.add(assembly_plan) + product_doc.add(assembly_activity) + + usages = list(assembly_activity.usages) + + # Expect: 1 reactant + at least 1 enzyme + self.assertTrue( + len(usages) >= 2, + "assembly activity should include reactant and enzyme usages", + ) + + entities = [u.entity for u in usages] + + # reactant implementation should be present + self.assertIn( + plasmid.plasmid_implementations[0].identity, + entities, + "Reactant implementation missing from activity usages", + ) + + # restriction enzyme should be present + self.assertIn( + self.re_impl.identity, + entities, + "Restriction enzyme missing from activity usages", + ) extract = parts_list[0][0] - self.assertEqual( - extract.roles, - ["https://identifiers.org/so/SO:0000915"], + self.assertTrue( + ENGINEERED_INSERT in extract.roles, "Part digestion extracted part missing engineered insert role", ) # engineered insert role self.assertTrue( - "http://identifiers.org/so/SO:0000987" in extract.types, + LINEAR in extract.types, "Part digestion extracted part missing linear DNA type", ) @@ -63,72 +141,60 @@ def test_part_digestion(self): ) else: self.assertTrue( - comp_def.identity in doc.componentDefinitions, + comp_def.identity in self.source_doc.componentDefinitions, "Digested part missing reference to part from original document", ) # check that old part has been transcribed to new doc, in extracted part - # check that wasderivedfroms match, assembly plan records all interactions, - contains_restriction, contains_reactant, contains_product = False, False, False - for participation in assembly_plan.interactions[0].participations: - if participation.displayId == "restriction": - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000019" - in participation.roles, - "Restriction participation missing 'modifier' role", - ) - contains_restriction = True - elif "reactant" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000010" - in participation.roles, - "Restriction reactant participation missing 'reactant' role", - ) - contains_reactant = True - elif "product" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000011" - in participation.roles, - "Restriction product participation missing 'product' role", - ) - contains_product = True - - self.assertTrue( - contains_product, "Digestion Assembly plan missing product participation" - ) - self.assertTrue( - contains_reactant, "Digestion Assembly plan missing reactant participation" - ) - self.assertTrue( - contains_restriction, - "Digestion Assembly plan missing restriction participation", - ) - sbol_validation_result = product_doc.validate() self.assertEqual( sbol_validation_result, "Valid.", "Part Digestion SBOL validation failed" ) def test_backbone_digestion(self): - doc = sbol2.Document() - doc.read("tests/test_files/backbone.xml") - - md = doc.getModuleDefinition("https://sbolcanvas.org/module1") - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + ) + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + assembly_activity = self.assembly.initialize_assembly_activity() - parts_list, assembly_plan = backbone_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, doc + parts_list, assembly_activity = backbone_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) product_doc = sbol2.Document() - for extract, sequence in parts_list: + for extract, _ in parts_list: product_doc.add(extract) - product_doc.add(sequence) - product_doc.add(assembly_plan) + product_doc.add(assembly_activity) + + usages = list(assembly_activity.usages) + + # Expect: 1 reactant + at least 1 enzyme + self.assertTrue( + len(usages) >= 2, + "Digestion activity should include reactant and enzyme usages", + ) + + entities = [u.entity for u in usages] + + # reactant implementation should be present + self.assertIn( + plasmid.plasmid_implementations[0].identity, + entities, + "Reactant implementation missing from activity usages", + ) + + # restriction enzyme should be present + self.assertIn( + self.re_impl.identity, + entities, + "Restriction enzyme missing from activity usages", + ) extract = parts_list[0][0] self.assertEqual( extract.roles, - ["https://identifiers.org/so/SO:0000755"], + [PLASMID_VECTOR], "Backbone digestion extracted part missing plasmid vector role", ) # plasmid vector @@ -141,57 +207,21 @@ def test_backbone_digestion(self): if "three_prime_oh" in comp_obj.displayId: self.assertEqual( comp_def.roles, - ["http://identifiers.org/so/SO:0001933"], + [THREE_PRIME_OVERHANG], "Part digestion missing 3 prime role", ) elif "five_prime_oh" in comp_obj.displayId: self.assertEqual( comp_def.roles, - ["http://identifiers.org/so/SO:0001932"], + [FIVE_PRIME_OVERHANG], "Part digestion missing 5 prime role", ) else: self.assertTrue( - comp_def.identity in doc.componentDefinitions, + comp_def.identity in self.source_doc.componentDefinitions, "Digested part missing reference to part from original document", ) # check that old part has been transcribed to new doc, in extracted part - # check that wasderivedfroms match, assembly plan records all interactions, - contains_restriction, contains_reactant, contains_product = False, False, False - for participation in assembly_plan.interactions[0].participations: - if participation.displayId == "restriction": - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000019" - in participation.roles, - "Restriction participation missing 'modifier' role", - ) - contains_restriction = True - elif "reactant" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000010" - in participation.roles, - "Restriction reactant participation missing 'reactant' role", - ) - contains_reactant = True - elif "product" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000011" - in participation.roles, - "Restriction product participation missing 'product' role", - ) - contains_product = True - - self.assertTrue( - contains_product, "Digestion Assembly plan missing product participation" - ) - self.assertTrue( - contains_reactant, "Digestion Assembly plan missing reactant participation" - ) - self.assertTrue( - contains_restriction, - "Digestion Assembly plan missing restriction participation", - ) - sbol_validation_result = product_doc.validate() self.assertEqual( sbol_validation_result, @@ -201,21 +231,29 @@ def test_backbone_digestion(self): def test_ligation(self): ligation_doc = sbol2.Document() - temp_doc = sbol2.Document() reactants_list = [] - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + assembly_activity = self.assembly.initialize_assembly_activity() parts = [ - "tests/test_files/pro_in_bb.xml", - "tests/test_files/rbs_in_bb.xml", - "tests/test_files/cds_in_bb.xml", - "tests/test_files/terminator_in_bb.xml", + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" + ), ] - for i, part in enumerate(parts): - temp_doc.read(part) - md = temp_doc.getModuleDefinition("https://sbolcanvas.org/module1") - extracts_tuple_list, assembly_plan = part_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, temp_doc + for i, impl in enumerate(parts): + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + + extracts_tuple_list, assembly_activity = part_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) for extract, sequence in extracts_tuple_list: @@ -230,11 +268,22 @@ def test_ligation(self): reactants_list.append(extracts_tuple_list[0][0]) - temp_doc.read("tests/test_files/backbone.xml") + backbone_impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + ) + # run digestion, extract component + sequence, add to ligation_doc, reactants_list - md = temp_doc.getModuleDefinition("https://sbolcanvas.org/module1") - extracts_tuple_list, assembly_plan = backbone_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, temp_doc + definition = self.source_doc.get(backbone_impl.built) + + self.sbh.pull( + "https://api.synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1", + self.source_doc, + ) + + plasmid = Plasmid(definition, None, [backbone_impl], None, self.source_doc) + + extracts_tuple_list, assembly_activity = backbone_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) for extract, seq in extracts_tuple_list: try: @@ -248,90 +297,88 @@ def test_ligation(self): else: print(e) - ligation_doc.add(assembly_plan) + ligation_doc.add(assembly_activity) reactants_list.append(extracts_tuple_list[0][0]) - ligation_doc.add(rebase_restriction_enzyme("BsaI")) - - pl = ligation(reactants_list, assembly_plan, ligation_doc) - - for p in pl: - for obj in p: - ligation_doc.add(obj) + ligation_doc.add_list([self.re_impl, self.ligase_impl]) - if type(obj) is sbol2.ComponentDefinition: - self.assertTrue( - "http://identifiers.org/so/SO:0000988" in obj.types, - "Ligation product missing circular DNA type", - ) - self.assertTrue( - "http://www.biopax.org/release/biopax-level3.owl#Dna" - in obj.types, - "Ligation product missing DNA Molecule type", - ) - self.assertTrue( - "http://identifiers.org/so/SO:0000804" in obj.roles, - "Ligation product missing engineered region role", - ) - - locations = [] - - for anno in obj.sequenceAnnotations: - for location in anno.locations: - locations.append( - (anno.identity, location.start, location.end) - ) + pull_uri = self.ligase_impl.built.replace( + "https://synbiohub.org", "https://api.synbiohub.org" + ) + self.sbh.pull(pull_uri, ligation_doc) - locations.sort(key=lambda x: x[1]) + final_doc = sbol2.Document() - for i in range(len(locations) - 1): - current_end = locations[i][2] - next_start = locations[i + 1][1] + composite_impls = ligation( + reactants_list, + assembly_activity, + "test", + ligation_doc, + final_doc, + self.ligase_impl, + ) - self.assertEqual( - current_end + 1, - next_start, - f"Mismatch in continuity: {locations[i][0]} ends at {current_end}, " - f"but {locations[i + 1][0]} starts at {next_start}", - ) + usages = list(assembly_activity.usages) + entities = [u.entity for u in usages] - sbol_validation_result = ligation_doc.validate() - self.assertEqual( - sbol_validation_result, "Valid.", "Ligation SBOL validation failed" + self.assertIn( + self.ligase_impl.identity, + entities, + "Ligase missing from assembly activity usages", ) - def test_golden_gate(self): - pro_doc = sbol2.Document() - pro_doc.read("tests/test_files/pro_in_bb.xml") - - rbs_doc = sbol2.Document() - rbs_doc.read("tests/test_files/rbs_in_bb.xml") + for part_impl in parts: + self.assertIn( + part_impl.identity, + entities, + f"{part_impl.displayId} missing from assembly activity usages", + ) - cds_doc = sbol2.Document() - cds_doc.read("tests/test_files/cds_in_bb.xml") + for i in composite_impls: + obj = final_doc.get(i.built) - ter_doc = sbol2.Document() - ter_doc.read("tests/test_files/terminator_in_bb.xml") + self.assertEqual( + i.wasGeneratedBy, + [assembly_activity.identity], + "Composite implementation not linked to assembly activity", + ) - bb_doc = sbol2.Document() - bb_doc.read("tests/test_files/backbone.xml") + if type(obj) is sbol2.ComponentDefinition: + self.assertTrue( + CIRCULAR in obj.types, + "Ligation product missing circular DNA type", + ) + self.assertTrue( + "http://www.biopax.org/release/biopax-level3.owl#Dna" in obj.types, + "Ligation product missing DNA Molecule type", + ) + self.assertTrue( + ENGINEERED_PLASMID in obj.roles, + "Ligation product missing engineered plasmid role", + ) - part_docs = [pro_doc, rbs_doc, cds_doc, ter_doc] + locations = [] - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "testassem", part_docs, bb_doc, "BsaI", assembly_doc - ) + for anno in obj.sequenceAnnotations: + for location in anno.locations: + locations.append((anno.identity, location.start, location.end)) - composites = assembly_obj.run(plasmids_in_module_definitions=True) + locations.sort(key=lambda x: x[1]) - self.assertEqual(len(composites), 1) + for i in range(len(locations) - 1): + current_end = locations[i][2] + next_start = locations[i + 1][1] - assembly_doc.write("validation_assembly.xml") + self.assertEqual( + current_end + 1, + next_start, + f"Mismatch in continuity: {locations[i][0]} ends at {current_end}, " + f"but {locations[i + 1][0]} starts at {next_start}", + ) - sbol_validation_result = assembly_doc.validate() + sbol_validation_result = final_doc.validate() self.assertEqual( - sbol_validation_result, "Valid.", "Assembly SBOL validation failed" + sbol_validation_result, "Valid.", "Ligation SBOL validation failed" ) diff --git a/tests/test_files/DVK_AE.xml b/tests/test_files/DVK_AE.xml new file mode 100644 index 0000000..d8ab2b6 --- /dev/null +++ b/tests/test_files/DVK_AE.xml @@ -0,0 +1,443 @@ + + + + + DVK_AE + 1 + DVK_AE + 2026-02-05T23:49:40 + + + + + + + + + + + Fusion_Site_E_4 + 1 + + + + + + + + + + + Fusion_Site_A_2 + 1 + + + + + + + + + + + dvk_backbone_core_5 + 1 + + + + + + + + + + + LacZ_cassette_3 + 1 + + + + + + + + + + + DVK_AEAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + DVK_AEAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 500 + + + + + + + + + + DVK_AEAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 501 + 504 + + + + + + + + + + DVK_AEAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 505 + 2731 + + + + + + + + + + DVK_AEConstraint1 + 1 + + + + + + + + + + + + DVK_AEConstraint2 + 1 + + + + + + + + + + + + DVK_AEConstraint3 + 1 + + + + + + + + + + + + + Fusion_Site_A + 1 + Fusion_Site_A + MoClo standard fusion site A + + + + + 26479688 + + + + + + + Fusion_Site_E + 1 + Fusion_Site_E + MoClo standard fusion site E + + + + + 26479688 + + + + + + + LacZ_cassette + 1 + LacZ_cassette + MoClo IPTG inducible LacZ alpha subunit + 2026-04-30T18:50:31 + + + + + 26479688 + + + + + + + dvk_backbone_core + 1 + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_kan + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 282 + 870 + + + + + + + + + + kan_region_annotation + 1 + + + + 26479688 + + + + kan + 1 + + + + 26479688 + 1103 + 1929 + + + + + + + + + + + kan_region + 1 + Kanamycin resistance gene from the pSB1K3 plasmid. + 2026-04-16T22:44:18 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + DVK_AE_sequence + 1 + + + + GGAGAGAGACCTGCACCATATGCGGTGTGAAATACCGCACAGATGCGTAAGGAGAAAATACCGCATCAGGCGCCATTCGCCATTCAGGCTGCGCAACTGTTGGGAAGGGCGATCGGTGCGGGCCTCTTCGCTATTACGCCAGCTGGCGAAAGGGGGATGTGCTGCAAGGCGATTAAGTTGGGTAACGCCAGGGTTTTCCCAGTCACGACGTTGTAAAACGACGGCCAGTGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGGTCTCTGCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + LacZ_cassette_sequence + 1 + LacZ_cassette Sequence + + + + AGAGACCTGCACCATATGCGGTGTGAAATACCGCACAGATGCGTAAGGAGAAAATACCGCATCAGGCGCCATTCGCCATTCAGGCTGCGCAACTGTTGGGAAGGGCGATCGGTGCGGGCCTCTTCGCTATTACGCCAGCTGGCGAAAGGGGGATGTGCTGCAAGGCGATTAAGTTGGGTAACGCCAGGGTTTTCCCAGTCACGACGTTGTAAAACGACGGCCAGTGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGGTCTCT + + + + + Fusion_Site_A_sequence + 1 + Fusion_Site_A Sequence + + + + GGAG + + + + + Fusion_Site_E_sequence + 1 + Fusion_Site_E Sequence + + + + GCTT + + + + + dvk_backbone_core_seq + 1 + + + + 26479688 + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + kan_region_seq + 1 + + + + 26479688 + caaggggtgttatgagccatattcaacgggaaacgtcttgctccaggccgcgattaaattccaacatggatgctgatttatatgggtataaatgggctcgcgataatgtcgggcaatcaggtgcgacaatctatcgattgtatgggaagcccgatgcgccagagttgtttctgaaacatggcaaaggtagcgttgccaatgatgttacagatgagatggtcagactaaactggctgacggaatttatgcctcttccgaccatcaagcattttatccgtactcctgatgatgcatggttactcaccactgcgatccccgggaaaacagcattccaggtattagaagaatatcctgattcaggtgaaaatattgttgatgcgctggcagtgttcctgcgccggttgcattcgattcctgtttgtaattgtccttttaacagcgatcgcgtatttcgtctcgctcaggcgcaatcacgaatgaataacggtttggttgatgcgagtgattttgatgacgagcgtaatggctggcctgttgaacaagtctggaaagaaatgcataagcttttgccattctcaccggattcagtcgtcactcatggtgatttctcacttgataaccttatttttgacgaggggaaattaataggttgtattgatgttggacgagtcggaatcgcagaccgataccaggatcttgccatcctatggaactgcctcggtgagttttctccttcattacagaaacggctttttcaaaaatatggtattgataatcctgatatgaataaattgcagtttcatttgatgctcgatgagtttttctaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/ExampleLvl2_design.xml b/tests/test_files/ExampleLvl2_design.xml new file mode 100644 index 0000000..968ccd3 --- /dev/null +++ b/tests/test_files/ExampleLvl2_design.xml @@ -0,0 +1,593 @@ + + + + + Two_genes + 1 + + + + + + + + Gen1_Component + 1 + + + + + + + + Gen_Component + 1 + + + + + + + + Two_genes_SequenceAnnotation + 1 + + + + Two_genes_SequenceAnnotation_Range + 1 + 1 + 906 + + + + + + + + + + Two_genes_SequenceAnnotation1 + 1 + + + + Two_genes_SequenceAnnotation1_Range + 1 + 907 + 1771 + + + + + + + + + + Two_genes_SequenceConstraint + 1 + + + + + + + + + + Gen1 + 1 + + + + + + + RBS1_Component + 1 + + + + + + + + Pro1_Component + 1 + + + + + + + + Ter1_Component + 1 + + + + + + + + CDS1_Component + 1 + + + + + + + + Gen1_SequenceAnnotation2 + 1 + + + + Gen1_SequenceAnnotation2_Range + 1 + 56 + 736 + + + + + + + + + + Gen1_SequenceAnnotation3 + 1 + + + + Gen1_SequenceAnnotation3_Range + 1 + 737 + 865 + + + + + + + + + + Gen1_SequenceAnnotation1 + 1 + + + + Gen1_SequenceAnnotation1_Range + 1 + 36 + 55 + + + + + + + + + + Gen1_SequenceAnnotation + 1 + + + + Gen1_SequenceAnnotation_Range + 1 + 1 + 35 + + + + + + + + + + Gen1_SequenceConstraint1 + 1 + + + + + + + + + Gen1_SequenceConstraint2 + 1 + + + + + + + + + Gen1_SequenceConstraint + 1 + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + B0032 + 1 + B0032 + MoClo Basic Part: RBS - Weiss RBS, medium strength. Modified from Bba_B0032 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + J23116 + 1 + J23116 + MoClo Basic Part: Constitutive promoter - Anderson series - low strength + + + + + 26479688 + + + + + + + E1010m_rfp + 1 + E1010m_rfp + MoClo Basic Part: CDS - Fluorescent protein. Red. Modified from Bba_E1010 to fix illegal sites. + + + + + 26479688 + + + + + + + B0033 + 1 + B0033 + MoClo Basic Part: RBS - Weiss RBS, low strength. Modified from Bba_B0033 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + E0040m_gfp + 1 + E0040m_gfp + MoClo Basic Part: CDS - Fluorescent protein. Green. Modified from Bba_E0040 to fix illegal site. + + + + + 26479688 + + + + + + + Gen + 1 + + + + + + + CDS_Component + 1 + + + + + + + + Pro_Component + 1 + + + + + + + + RBS_Component + 1 + + + + + + + + Ter_Component + 1 + + + + + + + + Gen_SequenceAnnotation2 + 1 + + + + Gen_SequenceAnnotation2_Range + 1 + 58 + 777 + + + + + + + + + + Gen_SequenceAnnotation + 1 + + + + Gen_SequenceAnnotation_Range + 1 + 1 + 35 + + + + + + + + + + Gen_SequenceAnnotation3 + 1 + + + + Gen_SequenceAnnotation3_Range + 1 + 778 + 906 + + + + + + + + + + Gen_SequenceAnnotation1 + 1 + + + + Gen_SequenceAnnotation1_Range + 1 + 36 + 57 + + + + + + + + + + Gen_SequenceConstraint2 + 1 + + + + + + + + + Gen_SequenceConstraint1 + 1 + + + + + + + + + Gen_SequenceConstraint + 1 + + + + + + + + + + J23100 + 1 + J23100 + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + + + 26479688 + + + + + + + Two_genesSequence + 1 + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGTCACACAGGAAAGTACTAATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATATTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGCAGAGTCACACAGGACTACTAATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + Gen1Sequence + 1 + + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGCAGAGTCACACAGGACTACTAATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + GenSequence + 1 + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGTCACACAGGAAAGTACTAATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0032_sequence + 1 + B0032 Sequence + + + + AGAGTCACACAGGAAAGTACTA + + + + + J23100_sequence + 1 + J23100 Sequence + + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + J23116_sequence + 1 + J23116 Sequence + + + + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + E0040m_gfp_sequence + 1 + E0040m_gfp Sequence + + + + ATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAA + + + + + E1010m_rfp_sequence + 1 + E1010m_rfp Sequence + + + + ATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAA + + + + + B0033_sequence + 1 + B0033 Sequence + + + + AGAGTCACACAGGACTACTA + + + + + Two_genes_SBOLDesignerActivity + 1 + Gonzalo Vidal + 2026-03-24T16:18:54.110-06:00 + + + + Association + 1 + + + + + + diff --git a/tests/test_files/comp_tu.xml b/tests/test_files/comp_tu.xml new file mode 100644 index 0000000..8a5a343 --- /dev/null +++ b/tests/test_files/comp_tu.xml @@ -0,0 +1,343 @@ + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + i2mLg6N9A + 1 + + + + + + + B0015_4 + 1 + + + + + + + + C0062_luxR_3 + 1 + + + + + + + + J23100_1 + 1 + + + + + + + + B0033_2 + 1 + + + + + + + + i2mLg6N9AAnnotation1 + 1 + + + + location1 + 1 + 36 + 55 + + + + + + + + + + i2mLg6N9AAnnotation2 + 1 + + + + location2 + 1 + 56 + 811 + + + + + + + + + + i2mLg6N9AAnnotation3 + 1 + + + + location3 + 1 + 812 + 940 + + + + + + + + + + i2mLg6N9AAnnotation0 + 1 + + + + location0 + 1 + 1 + 35 + + + + + + + + + + i2mLg6N9AConstraint3 + 1 + + + + + + + + + i2mLg6N9AConstraint1 + 1 + + + + + + + + + i2mLg6N9AConstraint2 + 1 + + + + + + + + + + B0033 + 1 + B0033 + MoClo Basic Part: RBS - Weiss RBS, low strength. Modified from Bba_B0033 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + J23100 + 1 + J23100 + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + + + 26479688 + + + + + + + C0062_luxR + 1 + C0062_luxR + MoClo Basic Part: CDS - Controller protein, luxR repressor/activator (in concert with HSL, represses pLuxR(pR) R0063. Also up-regulates pLuxR(pL) R0062) + + + + + 26479688 + + + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + i2mLg6N9A_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + J23100_sequence + 1 + J23100 Sequence + + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + C0062_luxR_sequence + 1 + C0062_luxR Sequence + + + + ATGAAAAACATAAATGCCGACGACACATACAGAATAATTAATAAAATTAAAGCTTGTAGAAGCAATAATGATATTAATCAATGCTTATCTGATATGACTAAAATGGTACATTGTGAATATTATTTACTCGCGATCATTTATCCTCATTCTATGGTTAAATCTGATATTTCAATCCTAGATAATTACCCTAAAAAATGGAGGCAATATTATGATGACGCTAATTTAATAAAATATGATCCTATAGTAGATTATTCTAACTCCAATCATTCACCAATTAATTGGAATATATTTGAAAACAATGCTGTAAATAAAAAATCTCCAAATGTAATTAAAGAAGCGAAAACATCAGGTCTTATCACTGGGTTTAGTTTCCCTATTCATACGGCTAACAATGGCTTCGGAATGCTTAGTTTTGCACATTCAGAAAAAGACAACTATATAGATAGTTTATTTTTACATGCGTGTATGAACATACCATTAATTGTTCCTTCTCTAGTTGATAATTATCGAAAAATAAATATAGCAAATAATAAATCAAACAACGATTTAACCAAAAGAGAAAAAGAATGTTTAGCGTGGGCATGCGAAGGAAAAAGCTCTTGGGATATTTCAAAAATATTAGGTTGCAGTGAGCGTACTGTCACTTTCCATTTAACCAATGCGCAAATGAAACTCAATACAACAAACCGCTGCCAAAGTATTTCTAAAGCAATTTTAACAGGAGCAATTGATTGCCCATACTTTAAAAATTAATAA + + + + + B0033_sequence + 1 + B0033 Sequence + + + + AGAGTCACACAGGACTACTA + + + + + J23100_Layout + + + + + B0015_Layout + + + + + C0062_luxR_Layout + + + + + B0033_Layout + + + + + i2mLg6N9A_Layout + + + + 455.0 + 334.5 + 200.0 + 100.0 + container + + + + + 0.0 + 50.0 + 200.0 + 1.0 + backbone + + + + + 0.0 + 0.0 + 50.0 + 100.0 + J23100_1 + + + + + + 50.0 + 0.0 + 50.0 + 100.0 + B0033_2 + + + + + + 100.0 + 0.0 + 50.0 + 100.0 + C0062_luxR_3 + + + + + + 150.0 + 0.0 + 50.0 + 100.0 + B0015_4 + + + + + diff --git a/tests/test_files/moclo_parts_circuit.xml b/tests/test_files/moclo_parts_circuit.xml index a71bac7..2c47e00 100644 --- a/tests/test_files/moclo_parts_circuit.xml +++ b/tests/test_files/moclo_parts_circuit.xml @@ -1,37 +1,35 @@ - - - - - moclo_circuit - - - - i6TQvNp91_28 - - - - - - - - - i6TQvNp91 + + + + + E0030_yfp + 1 + E0030_yfp + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 26479688 + + + + + + + qlSBuNBL 1 - - - E0030_yfp_3 + + + J23100_1 1 - + - - + + B0034_2 1 @@ -39,117 +37,137 @@ - - - J23100_1 + + + B0015_4 1 - + - - - B0015_4 + + + E0030_yfp_3 1 - + - - - i6TQvNp91Annotation2 + + + qlSBuNBLAnnotation3 1 - - - location2 + + + location3 1 + 780 + 908 - + - + - - - i6TQvNp91Annotation1 + + + qlSBuNBLAnnotation0 1 - - - location1 + + + location0 1 + 1 + 35 - + - + - - - i6TQvNp91Annotation0 + + + qlSBuNBLAnnotation2 1 - - - location0 + + + location2 1 + 57 + 779 - + - + - - - i6TQvNp91Annotation3 + + + qlSBuNBLAnnotation1 1 - - - location3 + + + location1 1 + 36 + 56 - + - + - - - i6TQvNp91Constraint2 + + + qlSBuNBLConstraint2 1 - - + + - - - i6TQvNp91Constraint3 + + + qlSBuNBLConstraint1 1 - - + + - - - i6TQvNp91Constraint1 + + + qlSBuNBLConstraint3 1 - - + + + + + + + B0034 + 1 + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + + + @@ -157,27 +175,10 @@ 1 B0015 MoClo Basic Part: Double terminator (B0010:B0012) - - - - 26479688 - - - - E0030_yfp - 1 - E0030_yfp - MoClo Basic Part: CDS - Fluorescent protein. Yellow. - - - - - 26479688 - - + @@ -185,68 +186,61 @@ 1 J23100 MoClo Basic Part: Constitutive promoter - Anderson series - high strength - - - - 26479688 + - - - B0034 - 1 - B0034 - MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. - - - - - 26479688 - - - + + + J23100_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + E0030_yfp_sequence + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + + + B0015_sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0034_sequence + AGAGAAAGAGGAGAAATACTA + + + + + qlSBuNBL_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGAAAGAGGAGAAATACTA + + J23100_Layout - - - moclo_circuit_Layout - + + + qlSBuNBL_Layout + - - 720.0 - 339.0 - 200.0 - 100.0 - i6TQvNp91_28 - - - - - - - B0015_Layout - - - - - i6TQvNp91_Layout - - - - 720.0 - 339.0 + + 412.5 + 337.0 200.0 100.0 container - + 0.0 50.0 200.0 @@ -255,7 +249,7 @@ - + 0.0 0.0 50.0 @@ -263,9 +257,9 @@ J23100_1 - + - + 50.0 0.0 50.0 @@ -273,9 +267,9 @@ B0034_2 - + - + 100.0 0.0 50.0 @@ -283,9 +277,9 @@ E0030_yfp_3 - + - + 150.0 0.0 50.0 @@ -293,7 +287,12 @@ B0015_4 - + + + + + B0015_Layout + @@ -305,4 +304,4 @@ B0034_Layout - + \ No newline at end of file diff --git a/tests/test_files/mocloparts116.xml b/tests/test_files/mocloparts116.xml new file mode 100644 index 0000000..d683a12 --- /dev/null +++ b/tests/test_files/mocloparts116.xml @@ -0,0 +1,307 @@ + + + + + E0030_yfp + 1 + E0030_yfp + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 26479688 + + + + + + + J23116 + 1 + J23116 + MoClo Basic Part: Constitutive promoter - Anderson series - low strength + 26479688 + + + + + + + B0034 + 1 + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + + + + + + + i0mwvNcgH + 1 + + + + + + + J23116_1 + 1 + + + + + + + + B0034_2 + 1 + + + + + + + + E0030_yfp_3 + 1 + + + + + + + + B0015_4 + 1 + + + + + + + + i0mwvNcgHAnnotation3 + 1 + + + + location3 + 1 + 780 + 908 + + + + + + + + + + i0mwvNcgHAnnotation1 + 1 + + + + location1 + 1 + 36 + 56 + + + + + + + + + + i0mwvNcgHAnnotation2 + 1 + + + + location2 + 1 + 57 + 779 + + + + + + + + + + i0mwvNcgHAnnotation0 + 1 + + + + location0 + 1 + 1 + 35 + + + + + + + + + + i0mwvNcgHConstraint3 + 1 + + + + + + + + + i0mwvNcgHConstraint1 + 1 + + + + + + + + + i0mwvNcgHConstraint2 + 1 + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + 26479688 + + + + + + + E0030_yfp_sequence + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + + + J23116_sequence + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + B0015_sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + i0mwvNcgH_sequence + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + B0034_sequence + AGAGAAAGAGGAGAAATACTA + + + + + J23116_Layout + + + + + B0015_Layout + + + + + i0mwvNcgH_Layout + + + + 437.5 + 337.0 + 200.0 + 100.0 + container + + + + + 0.0 + 50.0 + 200.0 + 1.0 + backbone + + + + + 0.0 + 0.0 + 50.0 + 100.0 + J23116_1 + + + + + + 50.0 + 0.0 + 50.0 + 100.0 + B0034_2 + + + + + + 100.0 + 0.0 + 50.0 + 100.0 + E0030_yfp_3 + + + + + + 150.0 + 0.0 + 50.0 + 100.0 + B0015_4 + + + + + + + E0030_yfp_Layout + + + + + B0034_Layout + + + \ No newline at end of file diff --git a/tests/test_files/pB0015_DE.xml b/tests/test_files/pB0015_DE.xml new file mode 100644 index 0000000..7379b8a --- /dev/null +++ b/tests/test_files/pB0015_DE.xml @@ -0,0 +1,440 @@ + + + + + pB0015_DE + 1 + pB0015_DE + + + + + + + + + + Fusion_Site_D_2 + 1 + + + + + + + + + + + B0015_3 + 1 + + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + Fusion_Site_E_4 + 1 + + + + + + + + + + + pB0015_DEAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pB0015_DEAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 133 + + + + + + + + + + pB0015_DEAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 134 + 137 + + + + + + + + + + pB0015_DEAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 138 + 2237 + + + + + + + + + + pB0015_DEConstraint1 + 1 + + + + + + + + + + + + pB0015_DEConstraint2 + 1 + + + + + + + + + + + + pB0015_DEConstraint3 + 1 + + + + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + Fusion_Site_D + 1 + Fusion_Site_D + MoClo standard fusion site D + + + + + 26479688 + + + + + + + Fusion_Site_E + 1 + Fusion_Site_E + MoClo standard fusion site E + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pB0015_DE_sequence + 1 + + + + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + Fusion_Site_D_sequence + 1 + Fusion_Site_D Sequence + + + + AGGT + + + + + Fusion_Site_E_sequence + 1 + Fusion_Site_E Sequence + + + + GCTT + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pB0032_BC.xml b/tests/test_files/pB0032_BC.xml new file mode 100644 index 0000000..53770bd --- /dev/null +++ b/tests/test_files/pB0032_BC.xml @@ -0,0 +1,440 @@ + + + + + pB0032_BC + 1 + pB0032_BC + + + + + + + + + + Fusion_Site_C_4 + 1 + + + + + + + + + + + Fusion_Site_B_2 + 1 + + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + B0032_3 + 1 + + + + + + + + + + + pB0032_BCAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pB0032_BCAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 26 + + + + + + + + + + pB0032_BCAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 27 + 30 + + + + + + + + + + pB0032_BCAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 31 + 2130 + + + + + + + + + + pB0032_BCConstraint1 + 1 + + + + + + + + + + + + pB0032_BCConstraint2 + 1 + + + + + + + + + + + + pB0032_BCConstraint3 + 1 + + + + + + + + + + + + + B0032 + 1 + B0032 + MoClo Basic Part: RBS - Weiss RBS, medium strength. Modified from Bba_B0032 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + Fusion_Site_B + 1 + Fusion_Site_B + MoClo standard fusion site B + + + + + 26479688 + + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pB0032_BC_sequence + 1 + + + + TACTAGAGTCACACAGGAAAGTACTAAATGagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + B0032_sequence + 1 + B0032 Sequence + + + + AGAGTCACACAGGAAAGTACTA + + + + + Fusion_Site_B_sequence + 1 + Fusion_Site_B Sequence + + + + TACT + + + + + Fusion_Site_C_sequence + 1 + Fusion_Site_C Sequence + + + + AATG + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pE0040_CD.xml b/tests/test_files/pE0040_CD.xml new file mode 100644 index 0000000..af9be9e --- /dev/null +++ b/tests/test_files/pE0040_CD.xml @@ -0,0 +1,440 @@ + + + + + pE0040_CD + 1 + pE0040_CD + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + Fusion_Site_D_4 + 1 + + + + + + + + + + + Fusion_Site_C_2 + 1 + + + + + + + + + + + E0040m_gfp_3 + 1 + + + + + + + + + + + pE0040_CDAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pE0040_CDAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 724 + + + + + + + + + + pE0040_CDAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 725 + 728 + + + + + + + + + + pE0040_CDAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 729 + 2828 + + + + + + + + + + pE0040_CDConstraint3 + 1 + + + + + + + + + + + + pE0040_CDConstraint1 + 1 + + + + + + + + + + + + pE0040_CDConstraint2 + 1 + + + + + + + + + + + + + E0040m_gfp + 1 + E0040m_gfp + MoClo Basic Part: CDS - Fluorescent protein. Green. Modified from Bba_E0040 to fix illegal site. + + + + + 26479688 + + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + + + 26479688 + + + + + + + Fusion_Site_D + 1 + Fusion_Site_D + MoClo standard fusion site D + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pE0040_CD_sequence + 1 + + + + AATGATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAAAGGTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + E0040m_gfp_sequence + 1 + E0040m_gfp Sequence + + + + ATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAA + + + + + Fusion_Site_C_sequence + 1 + Fusion_Site_C Sequence + + + + AATG + + + + + Fusion_Site_D_sequence + 1 + Fusion_Site_D Sequence + + + + AGGT + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pJ23100_AB.xml b/tests/test_files/pJ23100_AB.xml index d43bd38..f6174e8 100644 --- a/tests/test_files/pJ23100_AB.xml +++ b/tests/test_files/pJ23100_AB.xml @@ -4,9 +4,10 @@ pJ23100_AB 1 - pJ23100_AB + pJ23100_AB + @@ -17,6 +18,7 @@ 1 + @@ -28,6 +30,7 @@ 1 + @@ -39,6 +42,7 @@ 1 + @@ -50,6 +54,7 @@ 1 + @@ -61,6 +66,7 @@ 1 + @@ -68,6 +74,7 @@ 1 + 1 4 @@ -83,12 +90,14 @@ 1 + location1 1 + 5 39 @@ -104,6 +113,7 @@ pJ23100_ABAnnotation2 1 + @@ -111,6 +121,7 @@ location2 1 + 40 43 @@ -126,6 +137,7 @@ pJ23100_ABAnnotation3 1 + @@ -133,9 +145,10 @@ location3 1 + 44 - 2210 + 2143 @@ -149,6 +162,7 @@ 1 + @@ -161,6 +175,7 @@ 1 + @@ -173,6 +188,7 @@ 1 + @@ -188,6 +204,7 @@ MoClo standard fusion site A + 26479688 @@ -203,6 +220,7 @@ + 26479688 @@ -216,6 +234,7 @@ MoClo Basic Part: Constitutive promoter - Anderson series - high strength + 26479688 @@ -229,6 +248,7 @@ This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + 26479688 @@ -239,6 +259,7 @@ 1 + 26479688 @@ -251,6 +272,7 @@ 1 + 26479688 @@ -263,6 +285,7 @@ 1 + 26479688 @@ -271,6 +294,7 @@ 1 + 26479688 272 860 @@ -287,6 +311,7 @@ 1 + 26479688 @@ -295,6 +320,7 @@ 1 + 26479688 1031 1996 @@ -311,10 +337,13 @@ amp_region 1 Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + 26479688 + @@ -323,7 +352,9 @@ origin_of_replication_pSB1A2 1 Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + 26479688 @@ -336,7 +367,8 @@ 1 - GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTtccagtcgggaaacctgtcgtgccagctgcattaatgaatcggccaacgcgcggggaagacgtgcttagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca @@ -345,6 +377,7 @@ 1 Fusion_Site_A Sequence + GGAG @@ -355,6 +388,7 @@ 1 Fusion_Site_B Sequence + TACT @@ -365,6 +399,7 @@ 1 J23100 Sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC @@ -375,6 +410,7 @@ 1 + 26479688 agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca @@ -384,6 +420,7 @@ amp_region_seq 1 + 26479688 cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa @@ -395,6 +432,7 @@ 1 + 26479688 ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa diff --git a/tests/test_files/transformation_activity.xml b/tests/test_files/transformation_activity.xml new file mode 100644 index 0000000..d04a835 --- /dev/null +++ b/tests/test_files/transformation_activity.xml @@ -0,0 +1,1884 @@ + + + + + + + + + + 1 + pB0015_DE_digestion_product + + + 1 + + + + + 1 + pB0015_DE_reactant + + + + + + + + + + + 1 + pE0030_CD_digestion_product + + + + + + + + pJ23100_AB_reactant + 1 + + + + + + + pB0034_BC_digestion_product + + 1 + + + + + + + + + 1 + pB0034_BC_extracted_part_ligation + + + + + + composite_1_ligation + + + + + + + pE0030_CD_extracted_part_ligation + 1 + + + 1 + + + 1 + + + + composite_1_product + + + + + + pB0015_DE_extracted_part_ligation + + + 1 + + + + + 1 + + pJ23100_AB_extracted_part_ligation + + + + + + + + + 1 + + DVK_AE_extracted_backbone_ligation + + + + + + + + + 1 + + + composite_1 + + + + + + 1 + + + + + pB0034_BC_reactant + + + + + + 1 + + + + DVK_AE_backbone_reactant + + + + + + + + 1 + + BsaI_enzyme + + + + + + 1 + + + + 1 + + + DVK_AE_backbone_product + + + + + + + + restriction + 1 + + + + DVK_AE_digestion + + + + 1 + + + DVK_AE_backbone_reactant + + + + + + + + T4_Ligase + + + 1 + + + + + + + + restriction + 1 + + + + + + + + + + 1 + pB0034_BC_product + + + + 1 + pB0034_BC_digestion + + + + + 1 + pB0034_BC_reactant + + + + + + + + + + DVK_AE_backbone_digestion_product + + + 1 + + + + + + + + 1 + pJ23100_AB_digestion_product + + + + + + + + 1 + pJ23100_AB_digestion + + + + 1 + pJ23100_AB_product + + + + + + + + + + + 1 + pJ23100_AB_reactant + + + + + + + + 1 + + restriction + + + + + + + + pE0030_CD_reactant + + + 1 + + + + assembly_plan + + + pE0030_CD_digestion + + + + + + 1 + pE0030_CD_product + + + + + + + 1 + + + pE0030_CD_reactant + + + + + + + + restriction + 1 + + + + 1 + + + + + 1 + + + + 1 + + + pB0015_DE_product + + + pB0015_DE_digestion + + + + + restriction + 1 + + + + + + + + + + 1 + + pB0015_DE_reactant + + + + + + + + Escherichia coli, strain DH5alpha + + 1 + Ecoli_DH5a + + + Ecoli_DH5a + + + + + 1 + + + + composite_1_engineered_plasmid + + + + + 1 + + + + + Ecoli_DH5a_chassis + + + 1 + + + Ecoli_DH5a_with_composite_1 + + + + + BsaI + BsaI + Restriction enzyme BsaI from REBASE. + + + 1 + + + + + + pJ23100_AB_five_prime_oh_component + + 1 + + + + + + + + + + + 1 + + three_prime_oh_location + 40 + 43 + + + three_prime_overhang + + 1 + + + + 1 + + pJ23100_AB_extracted_part + + + + + 1 + J23100_3 + + + + + + + + + + + 1 + pJ23100_AB_part + + + 5 + 39 + + 1 + + pJ23100_AB_part_location + + + + + + + + + 1 + + + + pJ23100_AB_three_prime_oh_component + + + + + + + 1 + + + 1 + 1 + + 4 + + five_prime_oh_location + + + + five_prime_overhang + + + + + + 1 + + pJ23100_AB_five_prime_oh + + + + + + + + + 1 + Fusion_Site_A + MoClo standard fusion site A + + Fusion_Site_A + 26479688 + + + + + + + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + J23100 + + + + + J23100 + + + 26479688 + 1 + + + pJ23100_AB_three_prime_oh + + + + + 1 + + + + + + + + 26479688 + Fusion_Site_B + Fusion_Site_B + + MoClo standard fusion site B + + + + 1 + + + + + + + 1 + + + B0034_3 + + + + + + + + 1 + five_prime_overhang + + + + 1 + 1 + five_prime_oh_location + + + 4 + + + + + + + + + + 1 + + 26 + three_prime_oh_location + 29 + + + + three_prime_overhang + 1 + + + + + + + + + + 1 + + + + 1 + 25 + 5 + + pB0034_BC_part_location + + + pB0034_BC_part + + + + + + + 1 + + pB0034_BC_five_prime_oh_component + + + + + + 1 + pB0034_BC_three_prime_oh_component + + + + + + 1 + pB0034_BC_extracted_part + + + + + + + + + + 1 + + pB0034_BC_five_prime_oh + + + + + + + B0034 + + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + 1 + + + + + + + + 1 + pB0034_BC_three_prime_oh + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + 26479688 + + + + + + + + + + + + + 728 + three_prime_oh_location + + + 731 + 1 + + + 1 + three_prime_overhang + + + + + 1 + + + + + + 1 + + + + 727 + pE0030_CD_part_location + 1 + + 5 + + + pE0030_CD_part + + + + + + + + pE0030_CD_five_prime_oh_component + + 1 + + + + pE0030_CD_extracted_part + + + 1 + + + E0030_yfp_3 + + + + + + + + + + pE0030_CD_three_prime_oh_component + + + + 1 + + + + + + 1 + + + + + 4 + 1 + five_prime_oh_location + + 1 + + + + five_prime_overhang + + + + + + pE0030_CD_five_prime_oh + + + 1 + + + + + + 26479688 + + + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + + + + E0030_yfp + + + E0030_yfp + 1 + + + + + + + pE0030_CD_three_prime_oh + + + 1 + + + + + + MoClo standard fusion site D + + 1 + + Fusion_Site_D + + 26479688 + + Fusion_Site_D + + + + 1 + + + three_prime_overhang + + + 1 + + + 137 + + + 1 + 134 + three_prime_oh_location + + + + + + + + pB0015_DE_five_prime_oh_component + + + 1 + + + + + pB0015_DE_three_prime_oh_component + + 1 + + + + + + + + + + 1 + + B0015_3 + + + + + + + + + 1 + + + 4 + + + 1 + five_prime_oh_location + 1 + + + five_prime_overhang + + + + + + + + + pB0015_DE_extracted_part + + + pB0015_DE_part + + + + + 5 + pB0015_DE_part_location + 133 + + 1 + + + + 1 + + + + + + + + 1 + + + pB0015_DE_five_prime_oh + + + B0015 + + 26479688 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + + + 1 + B0015 + + + + + + 1 + + pB0015_DE_three_prime_oh + + + + + + Fusion_Site_E + + + 26479688 + + + 1 + + MoClo standard fusion site E + + + Fusion_Site_E + + + DVK_AE_extracted_backbone + + 1 + + + + DVK_AE_five_prime_oh_component + 1 + + + + + + + + + + + + + 1 + three_prime_oh_location + 2232 + 2235 + + + three_prime_overhang + 1 + + + + + + + + + + + 1 + + + dvk_backbone_core_5 + + + + + DVK_AE_backbone + + + + + 5 + 1 + DVK_AE_backbone_location + 2231 + + + + + 1 + + + + + DVK_AE_three_prime_oh_component + + 1 + + + + + + + + five_prime_overhang + + + 1 + five_prime_oh_location + + 1 + + 4 + + + + 1 + + + + + + + + DVK_AE_three_prime_oh + + + + + 1 + + + 1 + + DVK_AE_five_prime_oh + + + + + + + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + dvk_backbone_core + 26479688 + + 1 + + + kan_region_annotation + + + 1 + + + kan + 1929 + + 1103 + 26479688 + + + + + + + + 1 + + + 26479688 + + + + + 26479688 + + + + + + 1 + Component_ori + + + + + + + + + + + 870 + + + 1 + + + ori + + 282 + 26479688 + + + + + + 1 + origin_of_replication_pSB1A2_annotation + + + 26479688 + + + + + + 1 + Component_kan + + + 26479688 + + + + + + + + + + + + T4_Ligase + 1 + + T4_Ligase + + + Ligation_Scar_A + + + + + 1 + + + + + + 1 + Ligation_Scar_B + + + + + + + + + + + Ligation_Scar_C + + 1 + + + + + + + + 1 + + Ligation_Scar_D + + + + + + + 1 + + Ligation_Scar_E + + + + + + + + + + + 1 + + Ligation_Scar_E + + + + + 1 + Ligation_Scar_D + + + + + + + + dvk_backbone_core + + + + 1 + + + + + + + + 1 + + Ligation_Scar_A + + + + + + + 1 + + + 2270 + J23100_3_location + + + 1 + 2236 + + + J23100_3_annotation + + + + + Ligation_Scar_E_annotation + + + + + + 1 + 3026 + 3023 + Ligation_Scar_E_location + + + + 1 + + + + + + Ligation_Scar_C + + + 1 + + + composite_1 + + + + + E0030_yfp_3_annotation + + + 3022 + 1 + E0030_yfp_3_location + + + 2300 + + + 1 + + + + + 1 + + + B0015_3_annotation + + + 3155 + + 3027 + 1 + B0015_3_location + + + + + + + + 1 + + + E0030_yfp + + + + + composite_1 + + + + + Ligation_Scar_A_annotation + 1 + + + + + 1 + Ligation_Scar_A_location + 1 + 4 + + + + + + + 1 + + dvk_backbone_core_5_annotation + + + + 1 + + dvk_backbone_core_5_location + 5 + 2231 + + + + + + + + + + + + Ligation_Scar_D_location + 2299 + 2296 + 1 + + + 1 + + Ligation_Scar_D_annotation + + + + + + + + + J23100 + 1 + + + + + + + + B0034 + 1 + + + + 1 + + + B0034_3_annotation + + + B0034_3_location + + 2275 + 1 + 2295 + + + + + + 1 + + + + + + + Ligation_Scar_B_annotation + + + 2235 + + + Ligation_Scar_B_location + 2232 + 1 + + + 1 + + + + + + + + + B0015 + + 1 + + + + + + 1 + Ligation_Scar_B + + + + + + + + + Ligation_Scar_C_annotation + 1 + + + + 2274 + + 1 + Ligation_Scar_C_location + 2271 + + + + + + + + + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACT + 1 + pJ23100_AB_extracted_part_seq + + + 1 + + + pJ23100_AB_five_prime_oh_sequence + GGAG + + + + GGAG + + + + 1 + Fusion_Site_A Sequence + + Fusion_Site_A_sequence + + + + J23100_sequence + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + J23100 Sequence + + 1 + + + + + + + 1 + pJ23100_AB_three_prime_oh_sequence + TACT + + + + Fusion_Site_B Sequence + + 1 + TACT + + + Fusion_Site_B_sequence + + + + 1 + pB0034_BC_extracted_part_seq + TACTAGAGAAAGAGGAGAAATACTAAATG + + + + + 1 + TACT + + + pB0034_BC_five_prime_oh_sequence + + + + + B0034_sequence + + AGAGAAAGAGGAGAAATACTA + + + B0034 Sequence + 1 + + + + 1 + + pB0034_BC_three_prime_oh_sequence + + AATG + + + + Fusion_Site_C Sequence + AATG + + 1 + Fusion_Site_C_sequence + + + + + + + 1 + AATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGT + + pE0030_CD_extracted_part_seq + + + + + pE0030_CD_five_prime_oh_sequence + AATG + + 1 + + + + + 1 + + E0030_yfp_sequence + + + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + E0030_yfp Sequence + + + + + pE0030_CD_three_prime_oh_sequence + AGGT + 1 + + + + AGGT + 1 + Fusion_Site_D_sequence + + Fusion_Site_D Sequence + + + + + + + + pB0015_DE_extracted_part_seq + 1 + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTT + + + + + + 1 + + AGGT + pB0015_DE_five_prime_oh_sequence + + + + B0015 Sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + 1 + + B0015_sequence + + + + GCTT + + + pB0015_DE_three_prime_oh_sequence + 1 + + + + + + 1 + + + Fusion_Site_E Sequence + GCTT + + Fusion_Site_E_sequence + + + + + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAG + DVK_AE_extracted_backbone_seq + 1 + + + + DVK_AE_three_prime_oh_sequence + GGAG + 1 + + + + + GCTT + + + 1 + + DVK_AE_five_prime_oh_sequence + + + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + 26479688 + 1 + + + + + dvk_backbone_core_seq + + + + 1 + Ligation_Scar_A_sequence + GCTT + + + + + + Ligation_Scar_B_sequence + GGAG + + 1 + + + Ligation_Scar_C_sequence + + + 1 + TACT + + + + AATG + + Ligation_Scar_D_sequence + 1 + + + 1 + + + Ligation_Scar_E_sequence + AGGT + + + 1 + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTAGAGAAAGAGGAGAAATACTAAATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + composite_1_seq + + + + + composite_1_assembly_plan + 1 + + + + Ecoli_DH5a_with_composite_1_transformation_plan + TODO: generate accurate description of transformation + + 1 + + + buildcompiler + 1 + + + + + + + + + assemble_composite_1_design + 1 + + + + Golden Gate Assembly + assemble_composite_1 + 1 + + + + + 1 + + + assemble_composite_1_association + + + + + Bacterial Tranformation + 1 + transform_Ecoli_DH5a + + + + Ecoli_DH5a_chassis_source + + + 1 + + + + + + 1 + + + + transform_Ecoli_DH5a_association + + + + + + composite_1_plasmid_source + 1 + + + + + + + diff --git a/tests/test_files/transformation_activity_new.xml b/tests/test_files/transformation_activity_new.xml new file mode 100644 index 0000000..8f51315 --- /dev/null +++ b/tests/test_files/transformation_activity_new.xml @@ -0,0 +1,1884 @@ + + + + + + T4_Ligase + 1 + + + + + + + + + pE0030_CD_reactant + + + 1 + + + + + + 1 + + + + + pB0015_DE_reactant + + + + + + + + + 1 + DVK_AE_backbone_digestion_product + + + + + + 1 + pJ23100_AB_digestion_product + + + + + + + + + + + + 1 + pE0030_CD_extracted_part_ligation + + + + + + + + 1 + + composite_1_product + + + + + + + + + 1 + pB0034_BC_extracted_part_ligation + + + + + + pJ23100_AB_extracted_part_ligation + + + 1 + + + 1 + composite_1_ligation + + + + + + 1 + + DVK_AE_extracted_backbone_ligation + + + + + + + pB0015_DE_extracted_part_ligation + + 1 + + + + + + + pB0015_DE_digestion + + + 1 + + pB0015_DE_product + + + + + + + + + + 1 + pB0015_DE_reactant + + + + 1 + + + + 1 + restriction + + + + + + + + + + 1 + + + + DVK_AE_backbone_reactant + + + + + + + + pB0015_DE_digestion_product + + 1 + + + + 1 + + + BsaI_enzyme + + + + + 1 + + + + + + DVK_AE_digestion + + + + DVK_AE_backbone_reactant + + 1 + + + + + + 1 + + restriction + + + + + 1 + + + + DVK_AE_backbone_product + + + + 1 + + + + + + + 1 + + + + pJ23100_AB_reactant + + + + + + + + 1 + pJ23100_AB_reactant + + + + + + 1 + + + + + 1 + restriction + + + + + + + + + pJ23100_AB_product + + 1 + + + + pJ23100_AB_digestion + + + + + pB0034_BC_digestion + + + + 1 + + restriction + + + + + 1 + + + 1 + + + + pB0034_BC_product + + + + + + pB0034_BC_reactant + + + + 1 + + + + + assembly_plan + + + + pE0030_CD_digestion_product + + + 1 + + + + + + + 1 + composite_1 + + + + + + + + + 1 + + + pB0034_BC_digestion_product + + + + + + + + + + restriction + + 1 + + + + 1 + + + + + pE0030_CD_reactant + + 1 + + + + + + + + + pE0030_CD_product + 1 + + + + pE0030_CD_digestion + + + + + + 1 + pB0034_BC_reactant + + + + + + + + 1 + Escherichia coli, strain DH5alpha + + + Ecoli_DH5a + Ecoli_DH5a + + + + + + + + + + + 1 + + composite_1_engineered_plasmid + + + + 1 + + + + + 1 + + Ecoli_DH5a_chassis + + + Ecoli_DH5a_with_composite_1 + + + + 1 + + + BsaI + Restriction enzyme BsaI from REBASE. + BsaI + + + + + + + + pJ23100_AB_five_prime_oh_component + + 1 + + + + + + pJ23100_AB_part + 1 + + + + 5 + + 1 + 39 + + pJ23100_AB_part_location + + + + + + + + + + + + + + J23100_3 + 1 + + + + pJ23100_AB_extracted_part + + + + 1 + pJ23100_AB_three_prime_oh_component + + + + + + + + + five_prime_overhang + + + + 4 + five_prime_oh_location + + + 1 + 1 + + + + 1 + + + + + + + three_prime_overhang + + + 43 + + 1 + + 40 + three_prime_oh_location + + + 1 + + + 1 + + + + + + + + 1 + + pJ23100_AB_five_prime_oh + + + + + + Fusion_Site_A + MoClo standard fusion site A + + Fusion_Site_A + + + + 1 + + 26479688 + + + + + + 1 + + J23100 + + + J23100 + + + + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + 26479688 + + + + pJ23100_AB_three_prime_oh + + + + + 1 + + + + + Fusion_Site_B + + + + 26479688 + + + + 1 + Fusion_Site_B + MoClo standard fusion site B + + + + + + + + + 1 + B0034_3 + + + + + + + + + + 1 + + 26 + 29 + three_prime_oh_location + + + + 1 + + three_prime_overhang + + + + + pB0034_BC_extracted_part + + + + + + + 1 + + 1 + 4 + five_prime_oh_location + + + + five_prime_overhang + + 1 + + + + 1 + + + + 1 + + + 25 + + pB0034_BC_part_location + 5 + + 1 + + + pB0034_BC_part + + + + + + + + pB0034_BC_five_prime_oh_component + 1 + + + + + + + + 1 + pB0034_BC_three_prime_oh_component + + + + + + + + + 1 + + pB0034_BC_five_prime_oh + + + + + + + B0034 + + + + 26479688 + + + + 1 + + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + B0034 + + + + + pB0034_BC_three_prime_oh + 1 + + + + + + Fusion_Site_C + Fusion_Site_C + 1 + + MoClo standard fusion site C + + + 26479688 + + + + + + + + + + 1 + + + pE0030_CD_part + + + 727 + + 5 + + pE0030_CD_part_location + 1 + + + + + + + + five_prime_overhang + + + + 4 + five_prime_oh_location + + 1 + 1 + + + 1 + + + + + + + + pE0030_CD_five_prime_oh_component + 1 + + + + pE0030_CD_extracted_part + + + + + 1 + + + + pE0030_CD_three_prime_oh_component + + + + 1 + + + + + + + E0030_yfp_3 + 1 + + + + + + + + + 1 + three_prime_overhang + + + 731 + 728 + + + 1 + three_prime_oh_location + + + + + + + + + + + pE0030_CD_five_prime_oh + + + + + 1 + + + E0030_yfp + + E0030_yfp + + + + 26479688 + + + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 1 + + + + + pE0030_CD_three_prime_oh + 1 + + + + + + + + + 26479688 + + Fusion_Site_D + Fusion_Site_D + + + + 1 + MoClo standard fusion site D + + + + + + + + + + three_prime_overhang + + + + three_prime_oh_location + + 134 + 1 + 137 + + + + 1 + + + + + + 1 + + + + pB0015_DE_three_prime_oh_component + + + 1 + pB0015_DE_extracted_part + + + 1 + + pB0015_DE_five_prime_oh_component + + + + + + + + 1 + + five_prime_overhang + + + + five_prime_oh_location + 1 + 1 + + 4 + + + + + + + + pB0015_DE_part + + + + + pB0015_DE_part_location + + + 133 + 5 + 1 + + + 1 + + + + + + 1 + + + + + + + B0015_3 + + + + + + + + + pB0015_DE_five_prime_oh + + + 1 + + + + 26479688 + 1 + + + B0015 + + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + B0015 + + + + + + + + + 1 + pB0015_DE_three_prime_oh + + + Fusion_Site_E + 26479688 + + Fusion_Site_E + + + + 1 + MoClo standard fusion site E + + + + + + + + + + 1 + + + + 5 + DVK_AE_backbone_location + + 2231 + 1 + + + + DVK_AE_backbone + + + + + + + DVK_AE_five_prime_oh_component + + + 1 + + + + + + + + three_prime_oh_location + 2235 + + 2232 + 1 + + + + three_prime_overhang + + + 1 + + + + + 1 + + + + + + 4 + 1 + five_prime_oh_location + 1 + + + five_prime_overhang + + + + + + 1 + + + + + + dvk_backbone_core_5 + + + + + DVK_AE_extracted_backbone + + + + + + DVK_AE_three_prime_oh_component + + + 1 + + + 1 + + + + + 1 + + + DVK_AE_three_prime_oh + + + + + + + DVK_AE_five_prime_oh + + 1 + + + + + + kan_region_annotation + 26479688 + + + + + 1103 + 26479688 + + + kan + + 1929 + 1 + + + + + + + + 1 + + + + + + + + 1 + + + 870 + + 282 + + + + ori + 26479688 + 1 + + + + + origin_of_replication_pSB1A2_annotation + + 26479688 + + + dvk_backbone_core + + + + 1 + 26479688 + + + + + + + Component_ori + + + + + 26479688 + 1 + + + + 26479688 + + + Component_kan + + 1 + + + + + + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + + + + + + T4_Ligase + 1 + + T4_Ligase + + + + + + + + + Ligation_Scar_A + 1 + + + + + + + + Ligation_Scar_B + 1 + + + + + + + + 1 + + + Ligation_Scar_C + + + + + + + + + Ligation_Scar_D + 1 + + + + + 1 + + + + + + Ligation_Scar_E + + + + + + + Ligation_Scar_E + + 1 + + + + + + + B0015 + + 1 + + + + + 1 + + Ligation_Scar_B_annotation + + + + 2232 + + Ligation_Scar_B_location + + 1 + 2235 + + + + + + + 1 + + Ligation_Scar_B + + + + + + + + + Ligation_Scar_A + 1 + + + + + + + + + + Ligation_Scar_D + 1 + + + 1 + + + B0034 + + + 1 + + + + composite_1 + + + + 1 + Ligation_Scar_C + + + + + + + 1 + + + + 1 + 2231 + 5 + + + dvk_backbone_core_5_location + + + + dvk_backbone_core_5_annotation + + + + + Ligation_Scar_A_annotation + + + 1 + Ligation_Scar_A_location + 4 + + + 1 + + + + + 1 + + + + + + + + 3155 + B0015_3_location + + 3027 + 1 + + + + B0015_3_annotation + + 1 + + + + + + 1 + + + + 1 + + 2270 + 2236 + + J23100_3_location + + + J23100_3_annotation + + + + + + Ligation_Scar_C_annotation + + + 1 + + + + 1 + 2274 + 2271 + + Ligation_Scar_C_location + + + + + + + + 1 + + E0030_yfp + + + + + + + + + Ligation_Scar_E_annotation + + + 3023 + Ligation_Scar_E_location + + 3026 + 1 + + + + 1 + + + + + + + J23100 + 1 + + + + + + composite_1 + + + dvk_backbone_core + + 1 + + + + + + + + + 1 + Ligation_Scar_D_location + + + 2296 + 2299 + + + + Ligation_Scar_D_annotation + + 1 + + + + + + + + + + 1 + E0030_yfp_3_location + 3022 + 2300 + + + 1 + + E0030_yfp_3_annotation + + + + + + B0034_3_annotation + + + 2275 + 2295 + + B0034_3_location + + 1 + + + + 1 + + + + + + + 1 + pJ23100_AB_extracted_part_seq + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACT + + + + + 1 + + pJ23100_AB_five_prime_oh_sequence + GGAG + + + + Fusion_Site_A_sequence + Fusion_Site_A Sequence + + + + GGAG + + + 1 + + + + + 1 + J23100_sequence + + J23100 Sequence + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + pJ23100_AB_three_prime_oh_sequence + TACT + + + 1 + + + Fusion_Site_B_sequence + + Fusion_Site_B Sequence + + TACT + + 1 + + + + + + 1 + pB0034_BC_extracted_part_seq + + TACTAGAGAAAGAGGAGAAATACTAAATG + + + pB0034_BC_five_prime_oh_sequence + + + TACT + + 1 + + + + + B0034_sequence + + 1 + AGAGAAAGAGGAGAAATACTA + B0034 Sequence + + + + + 1 + AATG + + pB0034_BC_three_prime_oh_sequence + + + + + Fusion_Site_C Sequence + + + AATG + Fusion_Site_C_sequence + + + + 1 + + + + + AATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGT + 1 + pE0030_CD_extracted_part_seq + + + AATG + + pE0030_CD_five_prime_oh_sequence + + + 1 + + + + E0030_yfp Sequence + + 1 + + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + E0030_yfp_sequence + + + + pE0030_CD_three_prime_oh_sequence + 1 + + AGGT + + + + + + 1 + + Fusion_Site_D Sequence + Fusion_Site_D_sequence + + AGGT + + + + + 1 + + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTT + pB0015_DE_extracted_part_seq + + + pB0015_DE_five_prime_oh_sequence + 1 + + + AGGT + + + + + + + + + 1 + B0015_sequence + B0015 Sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + 1 + + GCTT + + + pB0015_DE_three_prime_oh_sequence + + + 1 + GCTT + + + + + Fusion_Site_E_sequence + Fusion_Site_E Sequence + + + + 1 + DVK_AE_extracted_backbone_seq + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAG + + + + + + 1 + + + DVK_AE_three_prime_oh_sequence + GGAG + + + 1 + DVK_AE_five_prime_oh_sequence + + + GCTT + + + + + 26479688 + 1 + + + dvk_backbone_core_seq + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + + 1 + GCTT + + Ligation_Scar_A_sequence + + + + Ligation_Scar_B_sequence + GGAG + + 1 + + + + TACT + + 1 + Ligation_Scar_C_sequence + + + Ligation_Scar_D_sequence + 1 + + + AATG + + + Ligation_Scar_E_sequence + 1 + + AGGT + + + + + + composite_1_seq + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTAGAGAAAGAGGAGAAATACTAAATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + 1 + + + 1 + composite_1_assembly_plan + + + + Ecoli_DH5a_with_composite_1_transformation_plan + + 1 + TODO: generate accurate description of transformation + + + 1 + + buildcompiler + + + + + + + + assemble_composite_1_design + + + 1 + + + Golden Gate Assembly + assemble_composite_1 + 1 + + + + 1 + + assemble_composite_1_association + + + + + + transform_Ecoli_DH5a + + + + + + 1 + composite_1_plasmid_source + + + + + + + transform_Ecoli_DH5a_association + + 1 + + + + + + + + + Ecoli_DH5a_chassis_source + 1 + + + + Bacterial Tranformation + 1 + + diff --git a/tests/test_full_build.py b/tests/test_full_build.py new file mode 100644 index 0000000..eb3a532 --- /dev/null +++ b/tests/test_full_build.py @@ -0,0 +1,178 @@ +import os +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID + + +class TestFullBuild(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + + def _make_part(self, display_id: str) -> sbol2.ComponentDefinition: + part = sbol2.ComponentDefinition(display_id) + self.doc.add(part) + return part + + def _get_by_display_id(self, display_id: str): + for obj in self.doc.componentDefinitions: + if obj.displayId == display_id: + return obj + return None + + def _make_design(self, display_id: str, part_ids: list[str]) -> sbol2.ComponentDefinition: + design = sbol2.ComponentDefinition(display_id) + self.doc.add(design) + created_components = [] + for index, part_id in enumerate(part_ids, start=1): + part = self._get_by_display_id(part_id) or self._make_part(part_id) + comp = design.components.create(f"{display_id}_c{index}") + comp.definition = part.identity + created_components.append(comp) + for index in range(len(created_components) - 1): + sc = design.sequenceConstraints.create(f"{display_id}_sc{index+1}") + sc.subject = created_components[index].identity + sc.object = created_components[index + 1].identity + sc.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + return design + + def _make_plasmid(self, display_id: str) -> sbol2.ComponentDefinition: + plasmid = sbol2.ComponentDefinition(display_id) + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + return plasmid + + def test_normalize_full_build_designs_input_shapes(self): + d1 = self._make_design("design_a", ["part_a"]) + d2 = self._make_design("design_b", ["part_b"]) + + self.assertEqual(self.compiler._normalize_full_build_designs(d1), [d1]) + self.assertEqual(self.compiler._normalize_full_build_designs([d1, d2]), [d1, d2]) + + derivation = sbol2.CombinatorialDerivation("combo") + self.doc.add(derivation) + with patch.object(self.compiler, "_expand_combinatorial_derivation", return_value=[d1]) as mock_expand: + normalized = self.compiler._normalize_full_build_designs(derivation) + self.assertEqual(normalized, [d1]) + mock_expand.assert_called_once_with(derivation) + + def test_expand_combinatorial_derivation_creates_deterministic_variants(self): + p1 = self._make_part("p1") + p2 = self._make_part("p2") + p3 = self._make_part("p3") + + template = self._make_design("master", ["p1", "p2"]) + derivation = sbol2.CombinatorialDerivation("combo_2") + derivation.masterTemplate = template.identity + + with patch("buildcompiler.buildcompiler.get_or_pull", return_value=template), patch( + "buildcompiler.buildcompiler.extract_combinatorial_design_parts", return_value={"a": [p1, p2], "b": [p3]} + ), patch( + "buildcompiler.buildcompiler.enumerate_design_variants", + return_value=[[p1, p3], [p2, p3]], + ): + variants = self.compiler._expand_combinatorial_derivation(derivation, product_name_prefix="combo") + + self.assertEqual([v.displayId for v in variants], ["combo_variant_001", "combo_variant_002"]) + self.assertIsNotNone(self._get_by_display_id("combo_variant_001")) + self.assertIsNotNone(self._get_by_display_id("combo_variant_002")) + + def test_find_missing_parts_reports_missing_and_present(self): + design = self._make_design("design_missing", ["part_x", "part_y"]) + part_x = self._get_by_display_id("part_x") + part_y = self._get_by_display_id("part_y") + + with patch.object(self.compiler, "_extract_design_parts", return_value=[part_x, part_y]), patch.object( + self.compiler, + "_construct_plasmid_dict", + return_value={"part_x": [object()], "part_y": []}, + ), patch.object(self.compiler, "_get_backbone", return_value=(object(), [object()])): + missing = self.compiler._find_missing_parts_for_lvl1(design) + + self.assertEqual(len(missing), 1) + self.assertEqual(missing[0]["part"].displayId, "part_y") + self.assertEqual(missing[0]["reason"], "no implemented plasmid") + + def test_full_build_orchestration_and_stage_skip(self): + design_a = self._make_design("dA", ["pa"]) + design_b = self._make_design("dB", ["pb"]) + missing_part = self._make_part("missing_part") + domesticated = self._make_plasmid("domesticated_missing_part") + assembled = self._make_plasmid("assembled_dA") + + with tempfile.TemporaryDirectory() as tmpdir, patch.object( + self.compiler, "_normalize_full_build_designs", return_value=[design_a, design_b] + ), patch.object( + self.compiler, + "_find_missing_parts_for_lvl1", + side_effect=[ + [{"part": missing_part, "reason": "no implemented plasmid"}], + [{"part": missing_part, "reason": "no implemented plasmid"}], + ], + ), patch.object(self.compiler, "domestication", return_value=[domesticated]) as mock_dom, patch.object( + self.compiler, "transformation", return_value={"stage": "transformation", "sbol_artifacts": []} + ) as mock_tx, patch.object( + self.compiler, "plating", return_value={"stage": "plating"} + ) as mock_plating, patch.object( + self.compiler, + "assembly_lvl1", + side_effect=[[assembled], RuntimeError("assembly fail")], + ) as mock_asm: + result = self.compiler.full_build(designs=[design_a, design_b], results_dir=tmpdir, overwrite=True) + + mock_dom.assert_called_once() + self.assertEqual(mock_asm.call_count, 2) + self.assertGreaterEqual(mock_tx.call_count, 2) + self.assertGreaterEqual(mock_plating.call_count, 2) + self.assertEqual(result["skipped"][0]["stage"], "assembly_lvl2") + self.assertEqual(len(result["assembly_lvl1"]["successful"]), 1) + self.assertEqual(len(result["assembly_lvl1"]["failed"]), 1) + + def test_full_build_writes_manifest_and_zip_and_return_shape(self): + design = self._make_design("d_main", ["p_main"]) + assembled = self._make_plasmid("assembled_main") + + with tempfile.TemporaryDirectory() as tmpdir, patch.object( + self.compiler, "_find_missing_parts_for_lvl1", return_value=[] + ), patch.object( + self.compiler, "assembly_lvl1", return_value=[assembled] + ), patch.object( + self.compiler, "transformation", return_value={"stage": "transformation", "sbol_artifacts": []} + ), patch.object( + self.compiler, "plating", return_value={"stage": "plating"} + ): + result = self.compiler.full_build(designs=[design], results_dir=Path(tmpdir) / "run", overwrite=True) + + manifest_path = Path(result["manifest_path"]) + zip_path = Path(result["zip_path"]) + + self.assertTrue(manifest_path.exists()) + self.assertTrue(zip_path.exists()) + self.assertIn("domestication", result) + self.assertIn("assembly_lvl1", result) + self.assertIn("transformation", result) + self.assertIn("plating", result) + self.assertIn("skipped", result) + + with zipfile.ZipFile(zip_path, "r") as archive: + names = archive.namelist() + self.assertIn("full_build_manifest.json", names) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5600e7f..6154f85 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,43 +1,81 @@ import sbol2 -import filecmp import os import sys -import tempfile import unittest -from pathlib import Path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.sbol2build import ( + rebase_restriction_enzyme, + dna_componentdefinition_with_sequence, + number_to_suffix, + is_circular, + append_extracts_to_doc, +) -from sbol2build import rebase_restriction_enzyme, dna_componentdefinition_with_sequence, number_to_suffix, is_circular, append_extracts_to_doc, part_in_backbone_from_sbol class Test_HelperFunctions(unittest.TestCase): def test_restriction_enzyme(self): bsai = rebase_restriction_enzyme(name="BsaI") - constructor_error = 'Constructor Error: ed_restriction_enzyme' + constructor_error = "Constructor Error: ed_restriction_enzyme" test_cases = [ - ('http://rebase.neb.com/rebase/enz/BsaI.html', bsai.wasDerivedFrom, constructor_error), - ('http://www.biopax.org/release/biopax-level3.owl#Protein', bsai.types, constructor_error), - ('BsaI', bsai.name, constructor_error), - ('Restriction enzyme BsaI from REBASE.', bsai.description, constructor_error) + ( + "http://rebase.neb.com/rebase/enz/BsaI.html", + bsai.wasDerivedFrom, + constructor_error, + ), + ( + "http://www.biopax.org/release/biopax-level3.owl#Protein", + bsai.types, + constructor_error, + ), + ("BsaI", bsai.name, constructor_error), + ( + "Restriction enzyme BsaI from REBASE.", + bsai.description, + constructor_error, + ), ] for expected, attribute, error_msg in test_cases: with self.subTest(expected=expected, attribute=attribute): self.assertIn(expected, attribute, error_msg) - def test_dna_component_and_sequence(self): + def test_dna_component_and_sequence(self): # create a test dna component - dna_identity = 'Test_dna_identity' - dna_sequence = 'Test_dna_sequence' - test_dna_component, test_sequence = dna_componentdefinition_with_sequence(dna_identity, dna_sequence) + dna_identity = "Test_dna_identity" + dna_sequence = "Test_dna_sequence" + test_dna_component, test_sequence = dna_componentdefinition_with_sequence( + dna_identity, dna_sequence + ) test_cases = [ - ("", repr(type(test_dna_component)), 'Constructor Error: dna_componentdefinition_with_sequence, Not a ComponentDefinition'), - ("", repr(type(test_sequence)), 'Constructor Error: dna_componentdefinition_with_sequence, Not a Sequence'), - (f"https://SBOL2Build.org/{dna_identity}_seq/1", test_sequence.identity, 'Constructor Error: dna_componentdefinition_with_sequence, Incorrect Identity'), - ([test_sequence.identity], test_dna_component.sequences, 'Constructor Error: dna_componentdefinition_with_sequence, Sequence not in ComponentDefinition Sequences List'), - (['http://www.biopax.org/release/biopax-level3.owl#DnaRegion'], test_dna_component.types, 'Constructor Error: dna_componentdefinition_with_sequence, Missing DNA type') + ( + "", + repr(type(test_dna_component)), + "Constructor Error: dna_componentdefinition_with_sequence, Not a ComponentDefinition", + ), + ( + "", + repr(type(test_sequence)), + "Constructor Error: dna_componentdefinition_with_sequence, Not a Sequence", + ), + ( + f"http://buildcompiler.org/{dna_identity}_seq/1", + test_sequence.identity, + "Constructor Error: dna_componentdefinition_with_sequence, Incorrect Identity", + ), + ( + [test_sequence.identity], + test_dna_component.sequences, + "Constructor Error: dna_componentdefinition_with_sequence, Sequence not in ComponentDefinition Sequences List", + ), + ( + ["http://www.biopax.org/release/biopax-level3.owl#DnaRegion"], + test_dna_component.types, + "Constructor Error: dna_componentdefinition_with_sequence, Missing DNA type", + ), ] for expected, attribute, error_msg in test_cases: @@ -46,7 +84,7 @@ def test_dna_component_and_sequence(self): def test_is_circular(self): comp_def_circ = sbol2.ComponentDefinition("test") - comp_def_circ.types = ['http://identifiers.org/so/SO:0000988'] + comp_def_circ.types = ["http://identifiers.org/so/SO:0000988"] comp_def_not_circ = sbol2.ComponentDefinition("test_not_circ") @@ -54,38 +92,39 @@ def test_is_circular(self): self.assertFalse(is_circular(comp_def_not_circ)) def test_number_to_suffix(self): - #1 letter + # 1 letter self.assertEqual(number_to_suffix(1), "A") self.assertEqual(number_to_suffix(2), "B") self.assertEqual(number_to_suffix(26), "Z") - #2 letters + # 2 letters self.assertEqual(number_to_suffix(27), "AA") self.assertEqual(number_to_suffix(28), "AB") self.assertEqual(number_to_suffix(52), "AZ") self.assertEqual(number_to_suffix(53), "BA") - #3 letters - self.assertEqual(number_to_suffix(702), "ZZ") # 26*27 - 1 + # 3 letters + self.assertEqual(number_to_suffix(702), "ZZ") # 26*27 - 1 self.assertEqual(number_to_suffix(703), "AAA") # 26*27 self.assertEqual(number_to_suffix(704), "AAB") # 26*27 + 1 self.assertEqual(number_to_suffix(0), "") def test_append_extracts_to_doc(self): doc = sbol2.Document() - tup1 = dna_componentdefinition_with_sequence('def1', 'atgcaatg') - tup2 = dna_componentdefinition_with_sequence('def2', 'ggacttaac') + tup1 = dna_componentdefinition_with_sequence("def1", "atgcaatg") + tup2 = dna_componentdefinition_with_sequence("def2", "ggacttaac") append_extracts_to_doc([tup1, tup2, tup1], doc) - #ensure duplicate of tup1 is not being counted + # ensure duplicate of tup1 is not being counted self.assertEqual(len(doc.sequences), 2) self.assertEqual(len(doc.componentDefinitions), 2) - self.assertEqual(doc.sequences[0].elements, 'atgcaatg') - self.assertEqual(doc.componentDefinitions[0].displayId, 'def1') - self.assertEqual(doc.sequences[1].elements, 'ggacttaac') - self.assertEqual(doc.componentDefinitions[1].displayId, 'def2') + self.assertEqual(doc.sequences[0].elements, "atgcaatg") + self.assertEqual(doc.componentDefinitions[0].displayId, "def1") + self.assertEqual(doc.sequences[1].elements, "ggacttaac") + self.assertEqual(doc.componentDefinitions[1].displayId, "def2") + + # TODO test for part in backbone? - #TODO test for part in backbone? -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_package_scaffolding.py b/tests/test_package_scaffolding.py new file mode 100644 index 0000000..c8b9ec6 --- /dev/null +++ b/tests/test_package_scaffolding.py @@ -0,0 +1,15 @@ +"""Import smoke tests for clean-architecture package scaffolding.""" + + +def test_clean_architecture_package_imports() -> None: + import buildcompiler.api # noqa: F401 + import buildcompiler.domain # noqa: F401 + import buildcompiler.planning # noqa: F401 + import buildcompiler.execution # noqa: F401 + import buildcompiler.stages # noqa: F401 + import buildcompiler.sbol # noqa: F401 + import buildcompiler.inventory # noqa: F401 + import buildcompiler.adapters # noqa: F401 + import buildcompiler.adapters.pudu # noqa: F401 + import buildcompiler.adapters.opentrons # noqa: F401 + import buildcompiler.reporting # noqa: F401 diff --git a/tests/test_plating.py b/tests/test_plating.py new file mode 100644 index 0000000..9989538 --- /dev/null +++ b/tests/test_plating.py @@ -0,0 +1,129 @@ +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID, ORGANISM_STRAIN +from buildcompiler.robotutils import generate_96_well_positions, normalize_plating_input + + +class _ProcResult: + def __init__(self, returncode=0, stdout="", stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +class TestPlating(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + self._id_seed = 0 + + def _make_transform_result(self, count=1): + artifacts = [] + self._id_seed += 1 + prefix = f"t{self._id_seed}_" + for i in range(1, count + 1): + plasmid = sbol2.ComponentDefinition(f"{prefix}p{i}") + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + transform_module = sbol2.ModuleDefinition(f"{prefix}strain_{i}") + transform_module.roles = [ORGANISM_STRAIN] + self.doc.add(transform_module) + impl = sbol2.Implementation(f"{prefix}strain_{i}_impl") + impl.built = transform_module.identity + self.doc.add(impl) + artifacts.append( + { + "transformed_strain_module": transform_module.identity, + "transformed_strain_implementation": impl.identity, + } + ) + return {"stage": "transformation", "sbol_artifacts": artifacts} + + def test_normalization_shapes(self): + result = self._make_transform_result(2) + normalized = normalize_plating_input(result, doc=self.doc) + self.assertEqual(len(normalized), 2) + + sloc = normalize_plating_input({"strain_locations": {"A1": "x", "A2": "y"}}) + self.assertEqual(len(sloc), 2) + + bloc = normalize_plating_input({"bacterium_locations": {"A1": "x"}}) + self.assertEqual(len(bloc), 1) + + with self.assertRaises(ValueError): + normalize_plating_input({"invalid": True}) + + def test_plate_well_mapping_limits(self): + self.assertEqual(generate_96_well_positions(1), ["A1"]) + self.assertEqual(generate_96_well_positions(13)[-1], "B1") + self.assertEqual(len(generate_96_well_positions(96)), 96) + + with tempfile.TemporaryDirectory() as tmpdir: + ok = self.compiler.plating( + self._make_transform_result(96), Path(tmpdir), protocol_type="manual" + ) + self.assertEqual(len(ok["plate"]["plate_map"]), 96) + + with self.assertRaises(ValueError): + self.compiler.plating( + self._make_transform_result(97), Path(tmpdir), protocol_type="manual" + ) + + def test_plating_manual_outputs_and_provenance(self): + with tempfile.TemporaryDirectory() as tmpdir: + result = self.compiler.plating( + transformation_results=self._make_transform_result(2), + results_dir=tmpdir, + protocol_type="manual", + advanced_params={"incubation_temperature_c": 37}, + ) + self.assertEqual(result["stage"], "plating") + self.assertEqual(result["protocol_type"], "manual") + self.assertEqual(len(result["plate"]["plate_map"]), 2) + self.assertTrue( + Path(result["protocol_artifacts"]["manual_protocol_markdown"]).exists() + ) + self.assertTrue(Path(result["protocol_artifacts"]["plate_map_json"]).exists()) + self.assertTrue(Path(result["protocol_artifacts"]["plate_map_csv"]).exists()) + self.assertTrue( + Path(result["protocol_artifacts"]["plate_layout_dataframe_csv"]).exists() + ) + self.assertIn("pudu", result["protocol_artifacts"]) + + @patch("buildcompiler.robotutils.subprocess.run") + def test_plating_automated_script_and_sim_zip(self, mock_run): + mock_run.return_value = _ProcResult(returncode=0, stdout="ok", stderr="") + with tempfile.TemporaryDirectory() as tmpdir: + result = self.compiler.plating( + transformation_results=self._make_transform_result(1), + results_dir=tmpdir, + protocol_type="automated", + advanced_params={"replicates": 1}, + overwrite=True, + ) + script_path = Path(result["protocol_artifacts"]["ot2_script"]) + script = script_path.read_text(encoding="utf-8") + self.assertIn("def run(protocol: protocol_api.ProtocolContext):", script) + self.assertIn("from opentrons import protocol_api", script) + self.assertIn("json_params=ADVANCED_PARAMS", script) + self.assertNotIn("advanced_params=ADVANCED_PARAMS", script) + self.assertTrue(Path(result["protocol_artifacts"]["simulation_zip"]).exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/adapters/opentrons/test_simulation_boundary.py b/tests/unit/adapters/opentrons/test_simulation_boundary.py new file mode 100644 index 0000000..53fbf2c --- /dev/null +++ b/tests/unit/adapters/opentrons/test_simulation_boundary.py @@ -0,0 +1,38 @@ +import builtins +import sys + +import pytest + +from buildcompiler.adapters.opentrons import ( + OpentronsSimulationAdapter, + OptionalAutomationDependencyError, +) +from buildcompiler.api import ProtocolOptions + + +def test_opentrons_import_is_lazy(): + assert "opentrons" not in sys.modules + + +def test_simulate_false_does_not_import_opentrons(): + adapter = OpentronsSimulationAdapter() + + result = adapter.simulate("protocol.py", options=ProtocolOptions(simulate=False)) + + assert result.ran is False + assert "opentrons" not in sys.modules + + +def test_simulate_true_missing_dependency_raises(monkeypatch): + adapter = OpentronsSimulationAdapter() + + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "opentrons": + raise ImportError("forced missing dependency") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + with pytest.raises(OptionalAutomationDependencyError): + adapter.simulate("protocol.py", options=ProtocolOptions(simulate=True)) diff --git a/tests/unit/adapters/pudu/test_assembly_json.py b/tests/unit/adapters/pudu/test_assembly_json.py new file mode 100644 index 0000000..5b0859c --- /dev/null +++ b/tests/unit/adapters/pudu/test_assembly_json.py @@ -0,0 +1,26 @@ +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent + + +def test_assembly_route_to_pudu_json_shape_and_values(): + payload = assembly_route_to_pudu_json( + product_identity="https://example.org/products/p1", + part_plasmids=[ + IndexedPlasmid(identity="https://example.org/plasmids/part1"), + IndexedPlasmid(identity="https://example.org/plasmids/part2"), + ], + backbone=IndexedBackbone(identity="https://example.org/backbones/b1"), + restriction_enzyme=IndexedReagent( + identity="https://example.org/reagents/re1", name="BsaI" + ), + ) + + assert payload == { + "Product": "https://example.org/products/p1", + "Backbone": "https://example.org/backbones/b1", + "PartsList": [ + "https://example.org/plasmids/part1", + "https://example.org/plasmids/part2", + ], + "Restriction Enzyme": "BsaI", + } diff --git a/tests/unit/adapters/pudu/test_plating_json.py b/tests/unit/adapters/pudu/test_plating_json.py new file mode 100644 index 0000000..aa075e5 --- /dev/null +++ b/tests/unit/adapters/pudu/test_plating_json.py @@ -0,0 +1,19 @@ +from buildcompiler.adapters.pudu import plating_to_pudu_json + + +def test_plating_to_pudu_json_shape_and_values(): + payload = plating_to_pudu_json( + bacterium_locations={"strain_b": "B2", "strain_a": "A1"}, + advanced_parameters={"replicates": 2}, + ) + + assert payload == { + "bacterium_locations": {"strain_a": "A1", "strain_b": "B2"}, + "advanced_parameters": {"replicates": 2}, + } + + +def test_plating_to_pudu_json_defaults_advanced_parameters(): + payload = plating_to_pudu_json(bacterium_locations={"strain_a": "A1"}) + + assert payload["advanced_parameters"] == {} diff --git a/tests/unit/adapters/pudu/test_transformation_json.py b/tests/unit/adapters/pudu/test_transformation_json.py new file mode 100644 index 0000000..d18f7ab --- /dev/null +++ b/tests/unit/adapters/pudu/test_transformation_json.py @@ -0,0 +1,38 @@ +from buildcompiler.adapters.pudu import ( + transformation_to_pudu_json, + transformations_to_pudu_json, +) +from buildcompiler.domain import IndexedPlasmid + + +def test_transformation_to_pudu_json_shape_and_values(): + payload = transformation_to_pudu_json( + strain_identity="https://example.org/strain/s1", + chassis_identity="https://example.org/chassis/c1", + plasmids=[ + IndexedPlasmid(identity="https://example.org/plasmids/p1"), + "https://example.org/plasmids/p2", + ], + ) + + assert payload == { + "Strain": "https://example.org/strain/s1", + "Chassis": "https://example.org/chassis/c1", + "Plasmids": [ + "https://example.org/plasmids/p1", + "https://example.org/plasmids/p2", + ], + } + + +def test_transformations_to_pudu_json_batch_helper_is_deterministic(): + payloads = transformations_to_pudu_json( + strain_identities=["s1", "s2"], + chassis_identities=["c1", "c2"], + plasmid_sets=[["p1"], ["p2", "p3"]], + ) + + assert payloads == [ + {"Strain": "s1", "Chassis": "c1", "Plasmids": ["p1"]}, + {"Strain": "s2", "Chassis": "c2", "Plasmids": ["p2", "p3"]}, + ] diff --git a/tests/unit/adapters/test_protocol_boundaries.py b/tests/unit/adapters/test_protocol_boundaries.py new file mode 100644 index 0000000..266dbf6 --- /dev/null +++ b/tests/unit/adapters/test_protocol_boundaries.py @@ -0,0 +1,38 @@ +from buildcompiler.adapters import maybe_write_protocol_artifacts +from buildcompiler.api import ProtocolMode, ProtocolOptions + + +def test_protocol_mode_none_returns_in_memory_only(tmp_path): + artifacts = maybe_write_protocol_artifacts( + payloads={"assembly": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.NONE, results_dir=tmp_path), + basename="artifact", + ) + + assert artifacts["assembly"].path is None + assert artifacts["assembly"].content == {"k": "v"} + assert artifacts["assembly"].metadata["written"] is False + assert not any(tmp_path.iterdir()) + + +def test_protocol_mode_manual_writes_when_results_dir_set(tmp_path): + artifacts = maybe_write_protocol_artifacts( + payloads={"assembly": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.MANUAL, results_dir=tmp_path), + basename="artifact", + ) + + path = artifacts["assembly"].path + assert path is not None + assert path.name == "artifact_assembly.json" + assert path.exists() + + +def test_protocol_mode_automated_with_no_results_dir_writes_nothing(): + artifacts = maybe_write_protocol_artifacts( + payloads={"plating": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.AUTOMATED, results_dir=None), + ) + + assert artifacts["plating"].path is None + assert artifacts["plating"].metadata["written"] is False diff --git a/tests/unit/api/test_compiler_api.py b/tests/unit/api/test_compiler_api.py new file mode 100644 index 0000000..648d61e --- /dev/null +++ b/tests/unit/api/test_compiler_api.py @@ -0,0 +1,130 @@ +import sys + +import pytest + +from buildcompiler.api import BuildCompiler, BuildOptions, full_build + + +class FakePlanner: + def __init__(self): + self.calls = [] + + def plan(self, abstract_designs, *, options): + self.calls.append((abstract_designs, options)) + return {"plan": abstract_designs, "options": options} + + +class FakeExecutor: + def __init__(self): + self.calls = [] + + def execute(self, plan, *, options): + self.calls.append((plan, options)) + return {"result": plan, "options": options} + + +def test_import_smoke(): + assert BuildCompiler is not None + assert full_build is not None + assert BuildOptions is not None + + +def test_constructor_defaults_and_injected_dependencies(): + compiler = BuildCompiler() + assert compiler.inventory is None + assert compiler.sbol_document is None + assert compiler.planner is None + assert compiler.executor is None + assert compiler.adapters is None + assert isinstance(compiler.options, BuildOptions) + + inventory = object() + sbol_document = object() + planner = object() + executor = object() + adapters = object() + + injected = BuildCompiler( + inventory=inventory, + sbol_document=sbol_document, + planner=planner, + executor=executor, + adapters=adapters, + ) + + assert injected.inventory is inventory + assert injected.sbol_document is sbol_document + assert injected.planner is planner + assert injected.executor is executor + assert injected.adapters is adapters + + +def test_api_import_does_not_load_optional_automation_modules(): + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules + + +def test_from_synbiohub_placeholder_without_collection_loading(): + compiler = BuildCompiler.from_synbiohub( + collections=[], + sbh_registry=None, + auth_token=None, + sbol_doc=None, + ) + assert isinstance(compiler, BuildCompiler) + + +def test_from_synbiohub_raises_when_collection_loading_is_requested(): + with pytest.raises(NotImplementedError, match="collection loading/indexing"): + BuildCompiler.from_synbiohub(collections=["https://example.org/collection"]) + + +def test_execute_raises_clear_error_without_dependencies(): + compiler = BuildCompiler() + compiler.plan([object()]) + with pytest.raises(ValueError, match="inventory"): + compiler.execute({"plan": 1}) + + +def test_plan_execute_full_build_delegate_to_injected_dependencies(): + planner = FakePlanner() + executor = FakeExecutor() + options = BuildOptions() + compiler = BuildCompiler(planner=planner, executor=executor, options=options) + + plan = compiler.plan("design") + result = compiler.execute(plan) + full = compiler.full_build("design") + + assert plan["plan"] == "design" + assert result["result"]["plan"] == "design" + assert full["result"]["plan"] == "design" + assert planner.calls + assert executor.calls + + +def test_module_level_full_build_uses_lightweight_constructor_path(): + planner = FakePlanner() + executor = FakeExecutor() + + result = full_build("design", planner=planner, executor=executor) + + assert result["result"]["plan"] == "design" + + +def test_module_level_full_build_uses_synbiohub_factory_path_when_requested(): + planner = FakePlanner() + executor = FakeExecutor() + + result = full_build( + "design", + planner=planner, + executor=executor, + collections=[], + sbh_registry=None, + auth_token=None, + sbol_doc=None, + ) + + assert result["result"]["plan"] == "design" diff --git a/tests/unit/api/test_options.py b/tests/unit/api/test_options.py new file mode 100644 index 0000000..4ef7282 --- /dev/null +++ b/tests/unit/api/test_options.py @@ -0,0 +1,37 @@ +from buildcompiler.api import BuildOptions, ProtocolMode + + +def test_build_options_defaults_match_contract(): + options = BuildOptions() + + assert options.execution.max_iterations == 5 + assert options.execution.continue_on_error is False + assert options.protocol.mode == ProtocolMode.NONE + assert options.protocol.simulate is False + assert options.reagents.allow_reagent_purchase is False + assert options.reagents.default_restriction_enzyme == "BsaI" + assert options.reagents.default_ligase == "T4_DNA_ligase" + assert options.domestication.allow_sequence_domestication_edits is False + assert options.planning.combinatorial.max_variants == 256 + assert options.planning.combinatorial.allow_large_expansion is False + assert options.planning.lvl2_search.max_exhaustive_region_count == 4 + assert options.planning.lvl2_search.allow_large_order_search is False + assert options.reporting.include_rejected_routes is True + assert options.reporting.max_rejected_routes == 3 + assert options.approvals.approved_processes == set() + assert options.approvals.approved_approval_ids == set() + + +def test_mutable_defaults_are_isolated_across_instances(): + left = BuildOptions() + right = BuildOptions() + + left.approvals.approved_processes.add("biosafety") + left.approvals.approved_approval_ids.add("approval-1") + left.planning.combinatorial.max_variants = 1024 + left.reporting.max_rejected_routes = 10 + + assert right.approvals.approved_processes == set() + assert right.approvals.approved_approval_ids == set() + assert right.planning.combinatorial.max_variants == 256 + assert right.reporting.max_rejected_routes == 3 diff --git a/tests/unit/domain/test_contracts.py b/tests/unit/domain/test_contracts.py new file mode 100644 index 0000000..e85183e --- /dev/null +++ b/tests/unit/domain/test_contracts.py @@ -0,0 +1,84 @@ +from buildcompiler.domain import ( + ApprovalStatus, + BuildRequest, + BuildStage, + BuildStatus, + BuildWarning, + DesignKind, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) + + +def test_enum_values_and_planned_stages(): + assert StageStatus.BLOCKED.value == "blocked" + assert StageStatus.FAILED.value == "failed" + assert BuildStatus.SUCCESS.value == "success" + assert BuildStage.ASSEMBLY_LVL1.value == "assembly_lvl1" + assert BuildStage.DOMESTICATION.value == "domestication" + assert BuildStage.ASSEMBLY_LVL2.value == "assembly_lvl2" + assert BuildStage.TRANSFORMATION.value == "transformation" + assert BuildStage.PLATING.value == "plating" + + +def test_blocked_vs_failed_semantics_are_documented_as_contract_data(): + blocked = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/y", + missing_display_id="y", + missing_kind="cds", + required_stage=BuildStage.DOMESTICATION, + reason="Input could be generated by upstream stage", + ) + failed = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/z", + missing_display_id="z", + missing_kind="cds", + required_stage="fatal", + reason="Unsupported design cannot proceed without changes", + ) + + assert blocked.required_stage != "fatal" + assert failed.required_stage == "fatal" + + +def test_default_mutables_are_isolated_across_instances(): + req1 = BuildRequest("r1", BuildStage.DOMESTICATION, "a", None, DesignKind.COMPONENT_DEFINITION) + req2 = BuildRequest("r2", BuildStage.DOMESTICATION, "b", None, DesignKind.COMPONENT_DEFINITION) + req1.constraints["x"] = 1 + assert req2.constraints == {} + + s1 = StageResult("s1", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s2 = StageResult("s2", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s1.request_ids.append("r1") + s1.products.append(object()) + s1.missing_inputs.append( + MissingBuildInput(BuildStage.DOMESTICATION, "a", "b", None, "promoter", "fatal", "missing") + ) + s1.required_approvals.append(RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed")) + s1.warnings.append(BuildWarning("warn", "msg")) + s1.protocol_artifacts["x"] = "y" + s1.logs.append("log") + + assert s2.request_ids == [] + assert s2.products == [] + assert s2.missing_inputs == [] + assert s2.required_approvals == [] + assert s2.warnings == [] + assert s2.protocol_artifacts == {} + assert s2.logs == [] + + a1 = RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed") + a2 = RequiredApproval(ApprovalStatus.APPROVED, "biosafety", "granted") + a1.metadata["id"] = "1" + assert a2.metadata == {} + + w1 = BuildWarning("w1", "warning") + w2 = BuildWarning("w2", "warning") + w1.metadata["code"] = "X" + assert w2.metadata == {} diff --git a/tests/unit/execution/test_executor.py b/tests/unit/execution/test_executor.py new file mode 100644 index 0000000..f439dc3 --- /dev/null +++ b/tests/unit/execution/test_executor.py @@ -0,0 +1,147 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, fn): + self.fn = fn + self.calls = [] + + def run(self, request, *, source_document, target_document): + self.calls.append(request.id) + return self.fn(request) + + +def plasmid(identity): + return IndexedPlasmid( + identity=identity, + display_id=identity.split("/")[-1], + state=MaterialState.GENERATED, + ) + + +def test_imports_smoke(): + assert BuildContext + assert FullBuildExecutor + + +def test_promote_and_retry_flow(): + lvl2_count = {"n": 0} + + def lvl2(request): + lvl2_count["n"] += 1 + if lvl2_count["n"] == 1: + return StageResult( + id="r", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=[ + MissingBuildInput( + BuildStage.ASSEMBLY_LVL2, + request.source_identity, + "https://x/region", + "region", + "engineered_region", + BuildStage.ASSEMBLY_LVL1, + "missing", + ) + ], + ) + return StageResult( + id="r", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://x/lvl2")], + ) + + def lvl1(request): + return StageResult( + id="r1", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://x/region")], + ) + + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=BuildOptions(), + ) + ex = FullBuildExecutor( + context=ctx, + lvl2_stage=FakeStage(lvl2), + lvl1_stage=FakeStage(lvl1), + domestication_stage=FakeStage( + lambda r: StageResult( + id="d", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[r.id], + ) + ), + ) + plan = BuildPlan( + lvl2_requests=[ + BuildRequest( + id="req-l2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://x/mod", + source_display_id="mod", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ] + ) + result = ex.execute(plan) + assert result.status == BuildStatus.SUCCESS + assert any(r.stage == BuildStage.ASSEMBLY_LVL1 for r in result.stage_results) + assert len(result.final_products) == 2 + + +def test_max_iteration_stops(): + options = BuildOptions() + options.execution.max_iterations = 2 + blocked = FakeStage( + lambda r: StageResult( + id="b", stage=r.stage, status=StageStatus.BLOCKED, request_ids=[r.id] + ) + ) + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=options, + ) + ex = FullBuildExecutor( + context=ctx, lvl2_stage=blocked, lvl1_stage=blocked, domestication_stage=blocked + ) + plan = BuildPlan( + lvl2_requests=[ + BuildRequest( + id="req", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="x", + source_display_id="x", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ] + ) + result = ex.execute(plan) + assert result.status == BuildStatus.FAILED diff --git a/tests/unit/execution/test_executor_reporting.py b/tests/unit/execution/test_executor_reporting.py new file mode 100644 index 0000000..6bb55fb --- /dev/null +++ b/tests/unit/execution/test_executor_reporting.py @@ -0,0 +1,39 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildStatus +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver + + +class _NoopStage: + def run(self, request, *, source_document, target_document): + raise AssertionError("No stage runs expected") + + +def _executor(include_detailed_report: bool) -> FullBuildExecutor: + options = BuildOptions() + options.reporting.include_detailed_report = include_detailed_report + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=options, + ) + return FullBuildExecutor(context=ctx, lvl2_stage=_NoopStage(), lvl1_stage=_NoopStage(), domestication_stage=_NoopStage()) + + +def test_executor_always_returns_summary(): + result = _executor(False).execute(BuildPlan()) + assert result.summary is not None + assert result.summary.status == BuildStatus.SUCCESS + + +def test_executor_report_optional_off(): + result = _executor(False).execute(BuildPlan()) + assert result.report is None + + +def test_executor_report_optional_on(): + result = _executor(True).execute(BuildPlan()) + assert result.report is not None diff --git a/tests/unit/inventory/test_compatibility.py b/tests/unit/inventory/test_compatibility.py new file mode 100644 index 0000000..aa97a3d --- /dev/null +++ b/tests/unit/inventory/test_compatibility.py @@ -0,0 +1,13 @@ +from buildcompiler.inventory import RouteScore + + +def test_route_score_prefers_fewer_missing_domestications(): + assert RouteScore(missing_domestications=0).sort_key() < RouteScore(missing_domestications=1).sort_key() + + +def test_route_score_prefers_fewer_missing_lvl1_plasmids(): + assert RouteScore(missing_lvl1_plasmids=0).sort_key() < RouteScore(missing_lvl1_plasmids=1).sort_key() + + +def test_route_score_uses_stable_identity_tiebreak(): + assert RouteScore(identity_tiebreak=("a",)).sort_key() < RouteScore(identity_tiebreak=("b",)).sort_key() diff --git a/tests/unit/inventory/test_inventory_indexes.py b/tests/unit/inventory/test_inventory_indexes.py new file mode 100644 index 0000000..2ab1796 --- /dev/null +++ b/tests/unit/inventory/test_inventory_indexes.py @@ -0,0 +1,100 @@ +from buildcompiler.domain import BuildStage, IndexedBackbone, IndexedPlasmid, IndexedReagent, MaterialState +from buildcompiler.inventory import Inventory + + +def _plasmid(identity: str, inserts: list[str], fusion_sites=("A", "B"), antibiotic="Ampicillin", state=MaterialState.PLANNED): + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=state, + metadata={ + "insert_identities": inserts, + "fusion_sites": fusion_sites, + "antibiotic": antibiotic, + }, + ) + + +def test_inventory_indexes_and_queries_are_deterministic(): + p2 = _plasmid("https://example.org/p2", ["https://example.org/partA"], state=MaterialState.GENERATED) + p1 = _plasmid("https://example.org/p1", ["https://example.org/partA", "https://example.org/region1"]) + + b1 = IndexedBackbone( + identity="https://example.org/b1", + metadata={"fusion_sites": ("A", "B"), "antibiotic": "Ampicillin", "stage": BuildStage.ASSEMBLY_LVL1.value}, + ) + b2 = IndexedBackbone( + identity="https://example.org/b2", + metadata={"fusion_sites": ("A", "B"), "antibiotic": "Ampicillin", "stage": BuildStage.ASSEMBLY_LVL2.value}, + ) + + e1 = IndexedReagent(identity="https://example.org/r1", name="BsaI", reagent_type="restriction_enzyme") + l1 = IndexedReagent(identity="https://example.org/r2", name="T4_DNA_ligase", reagent_type="ligase") + + inv = Inventory(plasmids=[p2, p1], backbones=[b2, b1], reagents=[e1, l1]) + + assert inv.plasmids_by_identity[p1.identity] == p1 + assert [p.identity for p in inv.plasmids_by_insert_identity["https://example.org/partA"]] == [p1.identity, p2.identity] + assert [p.identity for p in inv.plasmids_by_fusion_sites[("A", "B")]] == [p1.identity, p2.identity] + assert [p.identity for p in inv.plasmids_by_antibiotic["Ampicillin"]] == [p1.identity, p2.identity] + + key = (("A", "B"), "Ampicillin") + assert [b.identity for b in inv.backbones_by_fusion_sites_and_antibiotic[key]] == [b1.identity, b2.identity] + assert inv.find_backbone(fusion_sites=("A", "B"), antibiotic="Ampicillin", stage=BuildStage.ASSEMBLY_LVL1) == b1 + + assert inv.find_restriction_enzyme("BsaI") == e1 + assert inv.find_ligase("T4_DNA_ligase") == l1 + assert inv.find_ligase().identity == l1.identity + + assert [p.identity for p in inv.find_single_part_plasmids("https://example.org/partA")] == [p1.identity, p2.identity] + assert [p.identity for p in inv.find_lvl1_region_plasmids("https://example.org/region1")] == [p1.identity] + assert inv.find_lvl1_region_plasmids("https://example.org/partA", min_material_state=MaterialState.GENERATED) == [p2] + + +def test_add_generated_product_updates_indexes_immediately(): + inv = Inventory() + product = _plasmid( + "https://example.org/generated1", + ["https://example.org/partG"], + fusion_sites=("C", "D"), + antibiotic="Kanamycin", + state=MaterialState.GENERATED, + ) + + inv.add_generated_product(product) + + assert inv.generated_products_by_identity[product.identity] == product + assert inv.plasmids_by_identity[product.identity] == product + assert inv.find_single_part_plasmids("https://example.org/partG") == [product] + assert inv.plasmids_by_fusion_sites[("C", "D")] == [product] + assert inv.plasmids_by_antibiotic["Kanamycin"] == [product] + + +def test_add_generated_product_replaces_existing_secondary_indexes(): + inv = Inventory() + original = _plasmid( + "https://example.org/generated2", + ["https://example.org/partOld"], + fusion_sites=("A", "B"), + antibiotic="Ampicillin", + state=MaterialState.GENERATED, + ) + updated = _plasmid( + "https://example.org/generated2", + ["https://example.org/partNew"], + fusion_sites=("C", "D"), + antibiotic="Kanamycin", + state=MaterialState.ASSEMBLED, + ) + + inv.add_generated_product(original) + inv.add_generated_product(updated) + + assert inv.plasmids_by_identity[updated.identity] == updated + assert inv.generated_products_by_identity[updated.identity] == updated + assert inv.find_single_part_plasmids("https://example.org/partOld") == [] + assert inv.find_single_part_plasmids("https://example.org/partNew") == [updated] + assert inv.plasmids_by_fusion_sites.get(("A", "B"), []) == [] + assert inv.plasmids_by_fusion_sites[("C", "D")] == [updated] + assert inv.plasmids_by_antibiotic.get("Ampicillin", []) == [] + assert inv.plasmids_by_antibiotic["Kanamycin"] == [updated] diff --git a/tests/unit/inventory/test_selector.py b/tests/unit/inventory/test_selector.py new file mode 100644 index 0000000..fe4c03c --- /dev/null +++ b/tests/unit/inventory/test_selector.py @@ -0,0 +1,73 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import IndexedPlasmid, MaterialState +from buildcompiler.inventory import CompatibilitySelector, Inventory + + +def _plasmid(identity: str, insert: str, *, state=MaterialState.PLANNED, source="collection") -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + state=state, + metadata={"insert_identities": [insert], "source": source, "antibiotic": "Ampicillin"}, + ) + + +def test_lvl1_missing_parts_are_reported_not_raised(): + inv = Inventory(plasmids=[_plasmid("https://e/p1", "https://e/part1")]) + sel = CompatibilitySelector(inv) + route = sel.select_lvl1_route(request_id="r1", part_identities=["https://e/part1", "https://e/part2"]).selected + assert route is not None + assert route.missing_part_identities == ("https://e/part2",) + + +def test_lvl1_prefers_existing_material_in_tie(): + inv = Inventory( + plasmids=[ + _plasmid("https://e/a", "https://e/part", source="generated", state=MaterialState.GENERATED), + _plasmid("https://e/b", "https://e/part", source="collection", state=MaterialState.GENERATED), + ] + ) + sel = CompatibilitySelector(inv) + route = sel.select_lvl1_route(request_id="r1", part_identities=["https://e/part"]).selected + assert route.selected_part_plasmids[0].identity == "https://e/b" + + +def test_lvl1_hard_constraints_override_selection_preference(): + inv = Inventory(plasmids=[_plasmid("https://e/a", "https://e/part", source="generated")]) + opts = BuildOptions() + opts.selection.prefer_existing_collection_material = True + sel = CompatibilitySelector(inv, options=opts) + route = sel.select_lvl1_route( + request_id="r1", + part_identities=["https://e/part"], + constraints={"allowed_identities": ["https://e/a"]}, + ).selected + assert route.selected_part_plasmids[0].identity == "https://e/a" + + +def test_lvl2_large_order_search_not_silent_without_opt_in(): + inv = Inventory() + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route(request_id="r2", region_identities=["a", "b", "c", "d", "e"]) + assert out.selected is None + assert out.rejected + + +def test_lvl2_rejected_alternatives_capped_at_3(): + inv = Inventory(plasmids=[_plasmid(f"https://e/p{i}", f"https://e/r{i}") for i in range(4)]) + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route(request_id="r3", region_identities=["https://e/r0", "https://e/r1", "https://e/r2", "https://e/r3"]) + assert out.selected is not None + assert len(out.rejected) == 3 + + +def test_lvl2_constrained_order_must_match_requested_regions(): + inv = Inventory(plasmids=[_plasmid("https://e/p0", "https://e/r0"), _plasmid("https://e/p1", "https://e/r1")]) + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route( + request_id="r4", + region_identities=["https://e/r0", "https://e/r1"], + constraints={"region_order": ["https://e/r0"]}, + ) + assert out.selected is None + assert out.rejected + assert out.rejected[0].missing_region_identities == ("https://e/r0", "https://e/r1") diff --git a/tests/unit/planning/test_classifier.py b/tests/unit/planning/test_classifier.py new file mode 100644 index 0000000..961e915 --- /dev/null +++ b/tests/unit/planning/test_classifier.py @@ -0,0 +1,55 @@ +import sbol2 + +from buildcompiler.domain import BuildStage +from buildcompiler.planning.classifier import classify_non_combinatorial, request_id_for +from buildcompiler.planning.models import UnsupportedPlanningRecord + + +def test_classifier_maps_module_and_components(): + sbol2.setHomespace("https://example.org") + md = sbol2.ModuleDefinition("https://example.org/mod") + out = classify_non_combinatorial(md) + assert out.stage == BuildStage.ASSEMBLY_LVL2 + + er = sbol2.ComponentDefinition("https://example.org/er") + p = sbol2.ComponentDefinition("https://example.org/p") + p.roles = ["http://identifiers.org/so/SO:0000167"] + r = sbol2.ComponentDefinition("https://example.org/r") + r.roles = ["http://identifiers.org/so/SO:0000139"] + c = sbol2.ComponentDefinition("https://example.org/c") + c.roles = ["http://identifiers.org/so/SO:0000316"] + t = sbol2.ComponentDefinition("https://example.org/t") + t.roles = ["http://identifiers.org/so/SO:0000141"] + er.components.create("c1").definition = p.identity + er.components.create("c2").definition = r.identity + er.components.create("c3").definition = c.identity + er.components.create("c4").definition = t.identity + out2 = classify_non_combinatorial(er) + assert out2.stage == BuildStage.ASSEMBLY_LVL1 + + +def test_classifier_warns_for_invalid_lvl1_part_mix(): + design = sbol2.ComponentDefinition("https://example.org/invalid") + p = sbol2.ComponentDefinition("https://example.org/p2") + p.roles = ["http://identifiers.org/so/SO:0000167"] + design.components.create("c1").definition = p.identity + design.components.create("c2").definition = p.identity + + out = classify_non_combinatorial(design) + assert isinstance(out, UnsupportedPlanningRecord) + assert "promoter" in out.reason.lower() + + +def test_classifier_domestication_and_unsupported_and_deterministic_id(): + part = sbol2.ComponentDefinition("https://example.org/part") + part.roles = ["http://identifiers.org/so/SO:0000139"] + out = classify_non_combinatorial(part) + assert out.stage == BuildStage.DOMESTICATION + + unknown = sbol2.ComponentDefinition("https://example.org/u") + bad = classify_non_combinatorial(unknown) + assert isinstance(bad, UnsupportedPlanningRecord) + + rid1 = request_id_for(BuildStage.ASSEMBLY_LVL1, "https://example.org/A", "A") + rid2 = request_id_for(BuildStage.ASSEMBLY_LVL1, "https://example.org/A", "A") + assert rid1 == rid2 diff --git a/tests/unit/planning/test_combinatorial.py b/tests/unit/planning/test_combinatorial.py new file mode 100644 index 0000000..ea54215 --- /dev/null +++ b/tests/unit/planning/test_combinatorial.py @@ -0,0 +1,84 @@ +import sbol2 +from buildcompiler.api import BuildOptions +from buildcompiler.planning.combinatorial import expand_combinatorial_derivation + +ROLE_URIS = [ + "http://identifiers.org/so/SO:0000167", + "http://identifiers.org/so/SO:0000139", + "http://identifiers.org/so/SO:0000316", + "http://identifiers.org/so/SO:0000141", +] + + +def _build_comb(valid=True): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + template = sbol2.ComponentDefinition("https://example.org/template") + doc.add(template) + comb = sbol2.CombinatorialDerivation("https://example.org/comb", template.identity) + doc.add(comb) + roles = ROLE_URIS if valid else ROLE_URIS[:3] + for i, role in enumerate(roles): + vc = comb.variableComponents.create(f"https://example.org/var{i}") + p = sbol2.ComponentDefinition(f"https://example.org/v{i}") + p.roles = [role] + doc.add(p) + vc.variants = [p.identity] + return comb + + +def test_expansion_and_blocking_behaviors(): + comb = _build_comb(valid=True) + reqs, unsupported, warnings = expand_combinatorial_derivation( + comb, options=BuildOptions() + ) + assert len(reqs) == 1 and unsupported == [] and reqs[0].variant_index == 0 + limited = BuildOptions() + limited.planning.combinatorial.max_variants = 0 + reqs2, unsupported2, warnings2 = expand_combinatorial_derivation( + comb, options=limited + ) + assert reqs2 == [] and unsupported2 + assert any(w.code == "planning.combinatorial.expansion_blocked" for w in warnings2) + invalid = _build_comb(valid=False) + reqs3, unsupported3, warnings3 = expand_combinatorial_derivation( + invalid, options=BuildOptions() + ) + assert reqs3 == [] and unsupported3 + assert any(w.code == "planning.combinatorial.invalid_variant" for w in warnings3) + + +def test_part_order_follows_template_sequence_not_variable_ids(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + template = sbol2.ComponentDefinition("https://example.org/template_seq") + doc.add(template) + template_components = [] + for idx in range(4): + component = template.components.create(f"t_component_{idx}") + component.definition = f"https://example.org/template_part_{idx}" + template_components.append(component) + comb = sbol2.CombinatorialDerivation( + "https://example.org/comb_seq", template.identity + ) + doc.add(comb) + variable_specs = [ + ("var_z", template_components[0], ROLE_URIS[0], "p0"), + ("var_y", template_components[1], ROLE_URIS[1], "p1"), + ("var_x", template_components[2], ROLE_URIS[2], "p2"), + ("var_w", template_components[3], ROLE_URIS[3], "p3"), + ] + expected_order = [] + for var_id, template_component, role, part_id in variable_specs: + vc = comb.variableComponents.create(var_id) + vc.variable = template_component.identity + part = sbol2.ComponentDefinition(f"https://example.org/{part_id}") + part.roles = [role] + doc.add(part) + expected_order.append(part.identity) + vc.variants = [part.identity] + reqs, unsupported, warnings = expand_combinatorial_derivation( + comb, options=BuildOptions() + ) + assert len(reqs) == 1 and unsupported == [] + assert reqs[0].constraints["part_order"] == expected_order diff --git a/tests/unit/planning/test_domestication_planner.py b/tests/unit/planning/test_domestication_planner.py new file mode 100644 index 0000000..031c1dd --- /dev/null +++ b/tests/unit/planning/test_domestication_planner.py @@ -0,0 +1,52 @@ +import sbol2 +import pytest + +from buildcompiler.constants import PART_ROLES +from buildcompiler.planning import DomesticationPlanner + + +def _part(identity: str, role: str, sequence: str | None = None) -> sbol2.ComponentDefinition: + doc = sbol2.Document() + part = sbol2.ComponentDefinition(identity) + part.roles = role + doc.addComponentDefinition(part) + if sequence is not None: + seq = sbol2.Sequence(f"{identity}_seq") + seq.elements = sequence + seq.encoding = sbol2.SBOL_ENCODING_IUPAC + doc.addSequence(seq) + part.sequences = seq.identity + return part + + +def test_supported_role_produces_plan() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/p", sorted(PART_ROLES)[0], "ATGCGT") + plan = planner.plan(part) + assert plan.part_identity == part.identity + assert plan.part_role in {"promoter", "rbs", "cds", "terminator"} + + +def test_unsupported_role_fails_structurally() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/x", "https://example.org/unsupported", "ATGC") + with pytest.raises(ValueError, match="Unsupported domestication role"): + planner.plan(part) + + +def test_missing_sequence_fails() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/p2", sorted(PART_ROLES)[0]) + with pytest.raises(ValueError, match="missing a usable DNA sequence"): + planner.plan(part) + + +def test_bsai_sites_create_edit_proposals_without_mutating_sequence() -> None: + planner = DomesticationPlanner() + original = "AAAGGTCTCTTT" + part = _part("https://example.org/p3", sorted(PART_ROLES)[0], original) + plan = planner.plan(part) + assert len(plan.sequence_edit_proposals) == 1 + assert plan.sequence_edit_proposals[0].site_sequence == "GGTCTC" + seq = part.doc.find(part.sequences[0]) + assert seq.elements == original diff --git a/tests/unit/planning/test_full_build_planner.py b/tests/unit/planning/test_full_build_planner.py new file mode 100644 index 0000000..4fb66a5 --- /dev/null +++ b/tests/unit/planning/test_full_build_planner.py @@ -0,0 +1,31 @@ +import sbol2 + +from buildcompiler.planning import FullBuildPlanner + + +def test_planner_mixed_inputs_returns_queues_and_warnings(): + sbol2.setHomespace("https://example.org") + mod = sbol2.ModuleDefinition("https://example.org/mod") + multi = sbol2.ComponentDefinition("https://example.org/multi") + p = sbol2.ComponentDefinition("https://example.org/p") + p.roles = ["http://identifiers.org/so/SO:0000167"] + r = sbol2.ComponentDefinition("https://example.org/r") + r.roles = ["http://identifiers.org/so/SO:0000139"] + c = sbol2.ComponentDefinition("https://example.org/c") + c.roles = ["http://identifiers.org/so/SO:0000316"] + t = sbol2.ComponentDefinition("https://example.org/t") + t.roles = ["http://identifiers.org/so/SO:0000141"] + multi.components.create("c1").definition = p.identity + multi.components.create("c2").definition = r.identity + multi.components.create("c3").definition = c.identity + multi.components.create("c4").definition = t.identity + dom = sbol2.ComponentDefinition("https://example.org/dom") + dom.roles = ["http://identifiers.org/so/SO:0000139"] + + planner = FullBuildPlanner() + plan = planner.plan([mod, multi, dom, object()]) + + assert len(plan.lvl2_requests) == 1 + assert len(plan.lvl1_requests) == 1 + assert len(plan.domestication_requests) == 1 + assert len(plan.unsupported) == 1 diff --git a/tests/unit/planning/test_validation.py b/tests/unit/planning/test_validation.py new file mode 100644 index 0000000..32b53ef --- /dev/null +++ b/tests/unit/planning/test_validation.py @@ -0,0 +1,49 @@ +import sbol2 +from buildcompiler.planning.validation import ( + ordered_lvl1_parts, + validate_lvl1_cardinality, +) + +ROLE_URIS = [ + "http://identifiers.org/so/SO:0000167", + "http://identifiers.org/so/SO:0000139", + "http://identifiers.org/so/SO:0000316", + "http://identifiers.org/so/SO:0000141", +] + + +def _mk_part(i, role, doc): + p = sbol2.ComponentDefinition(f"https://example.org/p{i}") + p.roles = [role] + doc.add(p) + return p + + +def _mk_lvl1(parts, doc): + d = sbol2.ComponentDefinition("https://example.org/lvl1") + for i, p in enumerate(parts): + d.components.create(f"c{i}").definition = p.identity + doc.add(d) + return d + + +def test_validate_cardinality_and_order_fallback_and_warning(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + parts = [_mk_part(i, r, doc) for i, r in enumerate(ROLE_URIS)] + lvl1 = _mk_lvl1(parts, doc) + ok, warnings = validate_lvl1_cardinality(lvl1) + assert ok is True + assert warnings == [] + ordered, ow = ordered_lvl1_parts(lvl1) + assert len(ordered) == 4 + + +def test_validate_missing_or_duplicate_role_fails(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + p = _mk_part(0, ROLE_URIS[0], doc) + lvl1 = _mk_lvl1([p, p, p, p], doc) + ok, warnings = validate_lvl1_cardinality(lvl1) + assert ok is False + assert any(w.code == "lvl1.invalid_cardinality" for w in warnings) diff --git a/tests/unit/reporting/conftest.py b/tests/unit/reporting/conftest.py new file mode 100644 index 0000000..e962df7 --- /dev/null +++ b/tests/unit/reporting/conftest.py @@ -0,0 +1,63 @@ +import pytest + +from buildcompiler.domain import ( + ApprovalStatus, + BuildStage, + BuildStatus, + BuildWarning, + FullBuildResult, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) + + +@pytest.fixture +def fake_full_build_result(): + def _make(status=BuildStatus.PARTIAL_SUCCESS, with_duplicates=False, with_routes=False): + p1 = IndexedPlasmid(identity="https://x/plasmidA", display_id="plasmidA", state=MaterialState.GENERATED) + products = [p1, p1] if with_duplicates else [p1] + missing = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity="https://x/mod", + missing_identity="https://x/regionA", + missing_display_id="regionA", + missing_kind="engineered_region", + required_stage=BuildStage.ASSEMBLY_LVL1, + reason="missing region", + ) + approval = RequiredApproval(status=ApprovalStatus.REQUIRED, process="biosafety", reason="needed") + warning = BuildWarning(code="w1", message="warn", stage=BuildStage.ASSEMBLY_LVL2) + artifacts = {} + if with_routes: + artifacts = {"selected_route": {"id": "route-1"}, "rejected_routes": [{"id": "route-2"}]} + stage_result = StageResult( + id="stage-1", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=["req-1"], + products=products, + missing_inputs=[missing], + required_approvals=[approval], + warnings=[warning], + protocol_artifacts=artifacts, + logs=["log1"], + ) + return FullBuildResult( + status=status, + plan=object(), + build_document=None, + stage_results=[stage_result], + graph=None, + final_products=products, + missing_inputs=[missing, missing] if with_duplicates else [missing], + required_approvals=[approval], + warnings=[warning], + summary=None, + report=None, + ) + + return _make diff --git a/tests/unit/reporting/test_graph.py b/tests/unit/reporting/test_graph.py new file mode 100644 index 0000000..92f4319 --- /dev/null +++ b/tests/unit/reporting/test_graph.py @@ -0,0 +1,25 @@ +from buildcompiler.reporting import BuildGraph, build_graph + + +def test_build_graph_contains_expected_nodes_edges(fake_full_build_result): + result = fake_full_build_result(with_duplicates=True) + graph = build_graph(result) + assert isinstance(graph, BuildGraph) + node_ids = {n.id for n in graph.nodes} + assert any(i.startswith("stage_result:") for i in node_ids) + assert any(i.startswith("missing:") for i in node_ids) + assert any(i.startswith("approval:") for i in node_ids) + rels = {(e.source, e.target, e.relationship) for e in graph.edges} + assert any(r[2] == "produces" for r in rels) + assert any(r[2] == "blocks" for r in rels) + + +def test_graph_dedup_and_json_safe_and_reporting_only(fake_full_build_result): + result = fake_full_build_result(with_duplicates=True) + before_stage_results = list(result.stage_results) + graph = build_graph(result) + assert len(graph.nodes) == len({n.id for n in graph.nodes}) + data = graph.to_dict() + assert isinstance(data["nodes"], list) + assert isinstance(graph.summary()["relationship_counts"], dict) + assert result.stage_results == before_stage_results diff --git a/tests/unit/reporting/test_report.py b/tests/unit/reporting/test_report.py new file mode 100644 index 0000000..3d8d685 --- /dev/null +++ b/tests/unit/reporting/test_report.py @@ -0,0 +1,38 @@ +from buildcompiler.reporting import BuildReport, build_graph, build_report + + +def test_build_report_includes_sections_routes_blockers_and_serialization(fake_full_build_result): + result = fake_full_build_result(with_routes=True) + graph = build_graph(result) + report = build_report(result, graph=graph) + assert isinstance(report, BuildReport) + assert report.stage_sections + assert report.selected_routes + assert report.rejected_alternatives + assert report.missing_inputs + assert report.required_approvals + assert report.warnings + assert report.next_actions + assert report.dependency_chain + assert report.graph_summary["node_count"] >= 1 + assert "blocked" in report.executive_summary.lower() + assert '"status"' in report.to_json() + assert "# Build Report" in report.to_markdown() + + +def test_build_report_deterministic(fake_full_build_result): + result = fake_full_build_result(with_routes=True) + graph = build_graph(result) + assert build_report(result, graph=graph).to_json() == build_report(result, graph=graph).to_json() + + +def test_build_report_failed_status_mentions_failure_without_blockers(fake_full_build_result): + result = fake_full_build_result(with_routes=False) + result.missing_inputs = [] + result.required_approvals = [] + result.status = result.status.FAILED + + report = build_report(result) + + assert "failed" in report.executive_summary.lower() + assert "completed" not in report.executive_summary.lower() diff --git a/tests/unit/reporting/test_summary.py b/tests/unit/reporting/test_summary.py new file mode 100644 index 0000000..c563a09 --- /dev/null +++ b/tests/unit/reporting/test_summary.py @@ -0,0 +1,21 @@ +from buildcompiler.domain import BuildStatus +from buildcompiler.reporting import BuildSummary, build_summary + + +def test_build_summary_counts_and_serializes(fake_full_build_result): + result = fake_full_build_result(status=BuildStatus.PARTIAL_SUCCESS) + summary = build_summary(result) + assert isinstance(summary, BuildSummary) + assert summary.status == BuildStatus.PARTIAL_SUCCESS + assert summary.final_product_count == len(result.final_products) + assert summary.missing_input_count == len(result.missing_inputs) + assert summary.required_approval_count == len(result.required_approvals) + assert summary.warning_count == len(result.warnings) + assert summary.to_dict()["status"] == "partial_success" + assert '"warning_count"' in summary.to_json() + assert "# Build Summary" in summary.to_markdown() + + +def test_build_summary_deterministic(fake_full_build_result): + result = fake_full_build_result() + assert build_summary(result).to_json() == build_summary(result).to_json() diff --git a/tests/unit/sbol/test_assembly_service.py b/tests/unit/sbol/test_assembly_service.py new file mode 100644 index 0000000..2ef8475 --- /dev/null +++ b/tests/unit/sbol/test_assembly_service.py @@ -0,0 +1,81 @@ +import sbol2 +import pytest + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) +from buildcompiler.sbol import AssemblyJob, AssemblySbolResult, AssemblyService + + +def test_assembly_service_runs_and_returns_normalized_products(monkeypatch): + source = sbol2.Document() + product_component = sbol2.ComponentDefinition("assembled_product") + source.add(product_component) + product_impl = sbol2.Implementation("assembled_product_impl") + product_impl.built = product_component.identity + source.add(product_impl) + product_impl.wasGeneratedBy = "https://example.org/activity/assembly" + + class FakeLegacyAssembly: + def __init__(self, **kwargs): + self.assembly_activity = sbol2.Activity("fake_assembly") + + def run(self, include_extracted_parts=False): + return [type("LegacyProduct", (), {"plasmid_definition": product_component, "plasmid_implementations": [product_impl]})()], source + + monkeypatch.setattr("buildcompiler.sbol.assembly.Assembly", FakeLegacyAssembly) + + service = AssemblyService() + result = service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity="https://example.org/products/p001", + product_display_id="p001", + part_plasmids=[ + IndexedPlasmid( + identity=product_component.identity, + sbol_component=product_component, + metadata={"implementation_identity": product_impl.identity}, + ) + ], + backbone=IndexedBackbone( + identity=product_component.identity, + sbol_component=product_component, + metadata={"implementation_identity": product_impl.identity}, + ), + restriction_enzyme=IndexedReagent(identity=product_impl.identity), + ligase=IndexedReagent(identity=product_impl.identity), + source_document=source, + target_document=sbol2.Document(), + ) + ) + + assert isinstance(result, AssemblySbolResult) + assert result.products + assert isinstance(result.products[0], IndexedPlasmid) + assert result.products[0].state == MaterialState.GENERATED + assert result.activity_identity + + +def test_assembly_service_raises_clear_error_for_missing_component(): + doc = sbol2.Document() + service = AssemblyService() + + with pytest.raises(ValueError, match="Missing SBOL ComponentDefinition"): + service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity="https://example.org/products/p001", + product_display_id="p001", + part_plasmids=[IndexedPlasmid(identity="https://example.org/missing")], + backbone=IndexedBackbone(identity="https://example.org/backbone"), + restriction_enzyme=IndexedReagent(identity="https://example.org/reagent/re"), + ligase=IndexedReagent(identity="https://example.org/reagent/ligase"), + source_document=doc, + target_document=sbol2.Document(), + ) + ) diff --git a/tests/unit/sbol/test_domestication_service.py b/tests/unit/sbol/test_domestication_service.py new file mode 100644 index 0000000..0ddfeae --- /dev/null +++ b/tests/unit/sbol/test_domestication_service.py @@ -0,0 +1,40 @@ +import sbol2 + +from buildcompiler.domain import IndexedBackbone, IndexedReagent, MaterialState +from buildcompiler.planning import SequenceEditProposal +from buildcompiler.sbol import DomesticationJob, DomesticationService + + +def test_domestication_service_returns_generated_plasmid_with_provenance() -> None: + source = sbol2.Document() + target = sbol2.Document() + part = sbol2.ComponentDefinition("https://example.org/part") + part.roles = ["https://example.org/role1", "https://example.org/role2"] + source.addComponentDefinition(part) + + service = DomesticationService() + result = service.run( + DomesticationJob( + part_identity=part.identity, + part_display_id="part", + part_component=part, + backbone=IndexedBackbone("https://example.org/bb", metadata={"stage": "domestication"}), + restriction_enzyme=IndexedReagent("https://example.org/bsai", name="BsaI", reagent_type="restriction_enzyme"), + ligase=IndexedReagent("https://example.org/lig", name="T4_DNA_ligase", reagent_type="ligase"), + source_document=source, + target_document=target, + sequence_edit_proposals=[ + SequenceEditProposal(part.identity, "BsaI", "GGTCTC", 5, "GGTCTC", "GGTCTA", "reason") + ], + ) + ) + + assert result.product.state == MaterialState.GENERATED + assert result.product.metadata["source_part_identity"] == part.identity + assert result.product.metadata["insert_identities"] == [part.identity] + assert result.product.roles == list(part.roles) + implementation_identity = result.product.metadata["implementation_identity"] + implementation = target.find(implementation_identity) + assert isinstance(implementation, sbol2.Implementation) + assert implementation.built == result.product.identity + assert result.logs diff --git a/tests/unit/sbol/test_resolver.py b/tests/unit/sbol/test_resolver.py new file mode 100644 index 0000000..f3b4297 --- /dev/null +++ b/tests/unit/sbol/test_resolver.py @@ -0,0 +1,109 @@ +import sbol2 +import pytest + +from buildcompiler.sbol import PullPolicy, SbolResolver + + +class FakePullClient: + def __init__(self, document: sbol2.Document) -> None: + self.document = document + self.calls: list[str] = [] + + def __call__(self, identity: str): + self.calls.append(identity) + return self.document.find(identity) + + +class ReturnOnlyPullClient: + def __init__(self, pulled_objects: dict[str, object]) -> None: + self.pulled_objects = pulled_objects + self.calls: list[str] = [] + + def __call__(self, identity: str): + self.calls.append(identity) + return self.pulled_objects.get(identity) + + +def _make_doc() -> tuple[sbol2.Document, dict[str, str]]: + doc = sbol2.Document() + ns = "https://example.org" + sbol2.setHomespace(ns) + + comp = sbol2.ComponentDefinition(f"{ns}/component") + mod = sbol2.ModuleDefinition(f"{ns}/module") + impl = sbol2.Implementation(f"{ns}/impl") + comb = sbol2.CombinatorialDerivation(f"{ns}/comb", comp.identity) + + doc.add(comp) + doc.add(mod) + doc.add(impl) + doc.add(comb) + return doc, { + "component": comp.identity, + "module": mod.identity, + "implementation": impl.identity, + "combinatorial": comb.identity, + } + + +def test_resolver_gets_expected_types_from_local_document(): + doc, ids = _make_doc() + resolver = SbolResolver(doc, pull_policy=PullPolicy.NEVER) + + assert resolver.get_component(ids["component"]).identity == ids["component"] + assert resolver.get_module(ids["module"]).identity == ids["module"] + assert resolver.get_implementation(ids["implementation"]).identity == ids["implementation"] + assert ( + resolver.get_combinatorial_derivation(ids["combinatorial"]).identity + == ids["combinatorial"] + ) + + +def test_never_policy_does_not_pull_on_miss(): + doc, _ = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.NEVER, pull_client=fake) + + with pytest.raises(LookupError): + resolver.get_component("https://example.org/missing") + + assert fake.calls == [] + + +def test_missing_only_pulls_only_when_local_lookup_misses(): + doc, ids = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.MISSING_ONLY, pull_client=fake) + + resolver.get_component(ids["component"]) + assert fake.calls == [] + + with pytest.raises(LookupError): + resolver.get_component("https://example.org/missing") + assert fake.calls == ["https://example.org/missing"] + + +def test_always_refresh_pulls_even_on_hit(): + doc, ids = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.ALWAYS_REFRESH, pull_client=fake) + + resolver.get_component(ids["component"]) + + assert fake.calls == [ids["component"]] + + +def test_missing_only_returns_object_from_pull_client_without_document_mutation(): + local_doc = sbol2.Document() + ns = "https://example.org" + sbol2.setHomespace(ns) + remote_component = sbol2.ComponentDefinition(f"{ns}/remote-component") + pull_client = ReturnOnlyPullClient({remote_component.identity: remote_component}) + resolver = SbolResolver( + local_doc, pull_policy=PullPolicy.MISSING_ONLY, pull_client=pull_client + ) + + resolved = resolver.get_component(remote_component.identity) + + assert resolved is remote_component + assert pull_client.calls == [remote_component.identity] diff --git a/tests/unit/stages/test_assembly_lvl1.py b/tests/unit/stages/test_assembly_lvl1.py new file mode 100644 index 0000000..1563525 --- /dev/null +++ b/tests/unit/stages/test_assembly_lvl1.py @@ -0,0 +1,186 @@ +import sbol2 + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildWarning, + DesignKind, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import AssemblySbolResult +from buildcompiler.stages import AssemblyLvl1Stage + + +class _FakeAssemblyService: + def __init__(self, products): + self.products = products + + def run(self, job): + return AssemblySbolResult( + products=self.products, + stage_document=job.target_document, + activity_identity="https://example.org/activity/a1", + logs=["fake-assembly-service-ran"], + ) + + +def _inventory(*, include_parts=True, include_backbone=True, include_reagents=True): + plasmids = [] + if include_parts: + plasmids = [ + IndexedPlasmid( + identity="https://example.org/plasmids/p-prom", + metadata={ + "insert_identities": ["https://example.org/parts/promoter"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-rbs", + metadata={ + "insert_identities": ["https://example.org/parts/rbs"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-cds", + metadata={ + "insert_identities": ["https://example.org/parts/cds"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-term", + metadata={ + "insert_identities": ["https://example.org/parts/terminator"], + "antibiotic": "Ampicillin", + }, + ), + ] + backbones = [] + if include_backbone: + backbones = [ + IndexedBackbone( + identity="https://example.org/backbones/lvl1", + metadata={ + "fusion_sites": ("A", "B"), + "antibiotic": "Ampicillin", + "stage": BuildStage.ASSEMBLY_LVL1.value, + }, + ) + ] + reagents = [] + if include_reagents: + reagents = [ + IndexedReagent( + identity="https://example.org/reagents/bsaI", + name="BsaI", + reagent_type="restriction_enzyme", + ), + IndexedReagent( + identity="https://example.org/reagents/ligase", + name="T4_DNA_ligase", + reagent_type="ligase", + ), + ] + return Inventory(plasmids=plasmids, backbones=backbones, reagents=reagents) + + +def _request(): + return BuildRequest( + id="req-1", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity="https://example.org/designs/d1", + source_display_id="d1", + source_kind=DesignKind.COMPONENT_DEFINITION, + constraints={ + "ordered_part_identities": [ + "https://example.org/parts/promoter", + "https://example.org/parts/rbs", + "https://example.org/parts/cds", + "https://example.org/parts/terminator", + ], + "fusion_sites": ["A", "B"], + "antibiotic": "Ampicillin", + "ordering_warnings": [ + { + "code": "planner.ordering", + "message": "planner warning", + "metadata": {"source": "planner"}, + } + ], + }, + ) + + +def test_assembly_lvl1_success_returns_products_json_and_indexes_generated(): + inv = _inventory() + generated = IndexedPlasmid( + identity="https://example.org/plasmids/generated-1", + state=MaterialState.GENERATED, + metadata={"insert_identities": ["https://example.org/parts/promoter"]}, + ) + stage = AssemblyLvl1Stage( + inventory=inv, assembly_service=_FakeAssemblyService([generated]) + ) + + result = stage.run( + _request(), source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS + assert result.products + assert result.products[0].identity == generated.identity + assert ( + result.json_intermediate + and result.json_intermediate["Product"] == "https://example.org/designs/d1" + ) + assert result.sbol_document is not None + assert inv.find_lvl1_region_plasmids("https://example.org/designs/d1") + assert result.logs + assert result.warnings and isinstance(result.warnings[0], BuildWarning) + + +def test_assembly_lvl1_missing_inputs_return_blocked_with_structured_kinds(): + inv = _inventory( + include_parts=False, include_backbone=False, include_reagents=False + ) + stage = AssemblyLvl1Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + result = stage.run( + _request(), source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.BLOCKED + kinds = {missing.missing_kind for missing in result.missing_inputs} + assert { + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + }.issubset(kinds) + + +def test_assembly_lvl1_accepts_planner_part_order_constraint(): + inv = _inventory() + stage = AssemblyLvl1Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + request = _request() + request.constraints = { + "part_order": request.constraints["ordered_part_identities"], + "fusion_sites": request.constraints["fusion_sites"], + "antibiotic": request.constraints["antibiotic"], + } + + result = stage.run( + request, source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py new file mode 100644 index 0000000..b0c7206 --- /dev/null +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -0,0 +1,297 @@ +import sbol2 + +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + DesignKind, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import AssemblySbolResult +from buildcompiler.stages import AssemblyLvl2Stage + + +class _FakeAssemblyService: + def __init__(self, products): + self.products = products + + def run(self, job): + return AssemblySbolResult( + products=self.products, + stage_document=job.target_document, + activity_identity="https://example.org/activity/lvl2", + logs=["fake-lvl2-assembly-service-ran"], + ) + + +def _module_doc(): + doc = sbol2.Document() + module = sbol2.ModuleDefinition("design_mod") + region1 = sbol2.ComponentDefinition("region1", sbol2.BIOPAX_DNA) + region2 = sbol2.ComponentDefinition("region2", sbol2.BIOPAX_DNA) + doc.addComponentDefinition(region1) + doc.addComponentDefinition(region2) + fc1 = module.functionalComponents.create("fc1") + fc1.definition = region1.identity + fc2 = module.functionalComponents.create("fc2") + fc2.definition = region2.identity + doc.addModuleDefinition(module) + return doc, module, [region1.identity, region2.identity] + + +def _request(source_identity, constraints=None): + return BuildRequest( + id="req-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity=source_identity, + source_display_id="design_mod", + source_kind=DesignKind.MODULE_DEFINITION, + constraints=constraints or {}, + ) + + +def _inventory( + region_identities=None, + *, + include_regions=True, + include_backbone=True, + include_reagents=True, +): + plasmids = [] + if include_regions and region_identities: + plasmids = [ + IndexedPlasmid( + identity="https://example.org/plasmids/lvl1-r1", + metadata={"insert_identities": [region_identities[0]]}, + state=MaterialState.ASSEMBLED, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/lvl1-r2", + metadata={"insert_identities": [region_identities[1]]}, + state=MaterialState.ASSEMBLED, + ), + ] + backbones = [] + if include_backbone: + backbones = [ + IndexedBackbone( + identity="https://example.org/backbones/lvl2", + metadata={"stage": BuildStage.ASSEMBLY_LVL2.value}, + ) + ] + reagents = [] + if include_reagents: + reagents = [ + IndexedReagent( + identity="https://example.org/reagents/bsaI", + name="BsaI", + reagent_type="restriction_enzyme", + ), + IndexedReagent( + identity="https://example.org/reagents/ligase", + name="T4_DNA_ligase", + reagent_type="ligase", + ), + ] + return Inventory(plasmids=plasmids, backbones=backbones, reagents=reagents) + + +def test_assembly_lvl2_success_routes_and_indexes_generated_product(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + generated = IndexedPlasmid( + identity="https://example.org/plasmids/lvl2-product", + state=MaterialState.GENERATED, + ) + stage = AssemblyLvl2Stage( + inventory=inv, assembly_service=_FakeAssemblyService([generated]) + ) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS + assert result.products and result.products[0].identity == generated.identity + assert ( + result.json_intermediate + and result.json_intermediate["Product"] == module.identity + ) + assert result.protocol_artifacts["selected_route"] is not None + assert inv.find_lvl1_region_plasmids(module.identity) + + +def test_assembly_lvl2_no_regions_failed(): + doc = sbol2.Document() + module = sbol2.ModuleDefinition("empty_mod") + doc.addModuleDefinition(module) + stage = AssemblyLvl2Stage(inventory=_inventory([], include_regions=False)) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.FAILED + + +def test_assembly_lvl2_missing_engineered_regions_promote_to_lvl1_blockers(): + doc, module, regions = _module_doc() + stage = AssemblyLvl2Stage(inventory=_inventory(regions, include_regions=False)) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.BLOCKED + engineered = [ + m for m in result.missing_inputs if m.missing_kind == "engineered_region" + ] + assert engineered + assert all(m.required_stage == BuildStage.ASSEMBLY_LVL1 for m in engineered) + + +def test_assembly_lvl2_large_order_requires_opt_in_without_explicit_order(): + options = BuildOptions() + options.planning.lvl2_search.max_exhaustive_region_count = 4 + options.planning.lvl2_search.allow_large_order_search = False + stage = AssemblyLvl2Stage( + inventory=_inventory([], include_regions=False), options=options + ) + constraints = {"region_identities": [f"https://example.org/r{i}" for i in range(5)]} + + doc, module, _ = _module_doc() + result = stage.run( + _request(module.identity, constraints=constraints), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.BLOCKED + assert result.protocol_artifacts["selected_route"] is None + + +def test_assembly_lvl2_region_order_constraint_is_hard(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + stage = AssemblyLvl2Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + order = [regions[1], regions[0]] + result = stage.run( + _request(module.identity, constraints={"region_order": order}), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.SUCCESS + assert result.protocol_artifacts["selected_route"]["region_order"] == order + + +def test_assembly_lvl2_incomplete_region_order_falls_back_with_warning(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + stage = AssemblyLvl2Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + incomplete_order = [regions[0]] + result = stage.run( + _request(module.identity, constraints={"region_order": incomplete_order}), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.SUCCESS + assert result.protocol_artifacts["selected_route"] is not None + assert any( + "Unable to satisfy region_order constraint" in log for log in result.logs + ) + + +def test_buildcompiler_assembly_lvl2_respects_supplied_backbone(monkeypatch): + from types import SimpleNamespace + + from buildcompiler.buildcompiler import BuildCompiler + import buildcompiler.buildcompiler as buildcompiler_module + + compiler = BuildCompiler.from_local_documents([]) + compiler.BbsI_impl = object() + compiler.T4_ligase_impl = object() + + lvl1_backbone = SimpleNamespace( + fusion_sites=buildcompiler_module.LVL2_FUSION_SITE_ORDER[0], + antibiotic_resistance=buildcompiler_module.KAN, + ) + supplied_lvl2_backbone = SimpleNamespace(name="caller-supplied-lvl2-backbone") + compiler.indexed_backbones = [lvl1_backbone] + + tu = SimpleNamespace(displayId="TU1") + lvl1_plasmid = SimpleNamespace( + plasmid_definition=SimpleNamespace(displayId="TU1_plasmid") + ) + captured = {} + + class FakeAssembly: + def __init__(self, plasmids, backbone, *args): + captured["plasmids"] = plasmids + captured["backbone"] = backbone + + def run(self): + return [ + SimpleNamespace( + plasmid_definition=SimpleNamespace(displayId="lvl2_product") + ) + ], sbol2.Document() + + def fail_get_backbone(*args, **kwargs): + raise AssertionError( + "assembly_lvl2 should not auto-select a backbone when one is supplied" + ) + + monkeypatch.setattr(buildcompiler_module, "_extract_lvl2_TUs", lambda doc: [tu]) + monkeypatch.setattr(buildcompiler_module, "Assembly", FakeAssembly) + monkeypatch.setattr( + compiler, + "assembly_lvl1", + lambda *args, **kwargs: ({"TU1": [object()]}, sbol2.Document()), + ) + monkeypatch.setattr( + compiler, "_encapsulate_TU", lambda composite: (lvl1_plasmid, []) + ) + monkeypatch.setattr(compiler, "_get_backbone", fail_get_backbone) + + products, _ = compiler.assembly_lvl2( + sbol2.Document(), backbone=supplied_lvl2_backbone, product_name="lvl2" + ) + + assert products + assert captured["backbone"] is supplied_lvl2_backbone + + +def test_extract_lvl2_tus_uses_top_level_composite_not_document_order(): + from buildcompiler.buildcompiler import _extract_lvl2_TUs + + doc = sbol2.Document() + tu1 = sbol2.ComponentDefinition("tu1", sbol2.BIOPAX_DNA) + tu2 = sbol2.ComponentDefinition("tu2", sbol2.BIOPAX_DNA) + composite = sbol2.ComponentDefinition("lvl2_design", sbol2.BIOPAX_DNA) + + # Add child TUs before the composite to ensure extraction does not depend on + # ComponentDefinition serialization/document order. + doc.addComponentDefinition(tu1) + doc.addComponentDefinition(tu2) + doc.addComponentDefinition(composite) + + tu1_component = composite.components.create("tu1_component") + tu1_component.definition = tu1.identity + tu2_component = composite.components.create("tu2_component") + tu2_component.definition = tu2.identity + + tu_order = composite.sequenceConstraints.create("tu1_before_tu2") + tu_order.subject = tu1_component.identity + tu_order.object = tu2_component.identity + tu_order.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + + assert _extract_lvl2_TUs(doc) == [tu1, tu2] diff --git a/tests/unit/stages/test_domestication_stage.py b/tests/unit/stages/test_domestication_stage.py new file mode 100644 index 0000000..8b7b993 --- /dev/null +++ b/tests/unit/stages/test_domestication_stage.py @@ -0,0 +1,71 @@ +import sbol2 + +from buildcompiler.api import BuildOptions, ProtocolMode +from buildcompiler.domain import BuildRequest, BuildStage, DesignKind, IndexedBackbone, IndexedReagent, StageStatus +from buildcompiler.inventory import Inventory +from buildcompiler.stages import DomesticationStage + + +def _request(identity: str = "part") -> BuildRequest: + return BuildRequest("req-1", BuildStage.DOMESTICATION, identity, "part", DesignKind.COMPONENT_DEFINITION) + + +def _source_doc(identity: str = "part", seq: str = "ATGGGTCTCAA") -> sbol2.Document: + doc = sbol2.Document() + part = sbol2.ComponentDefinition(identity) + part.roles = "http://identifiers.org/so/SO:0000167" + seq_obj = sbol2.Sequence(f"{identity}_seq") + seq_obj.elements = seq + seq_obj.encoding = sbol2.SBOL_ENCODING_IUPAC + doc.addSequence(seq_obj) + part.sequences = seq_obj.identity + doc.addComponentDefinition(part) + return doc + + +def _inventory(with_backbone=True, with_enzyme=True, with_ligase=True) -> Inventory: + backbones = [IndexedBackbone("bb", metadata={"stage": BuildStage.DOMESTICATION.value})] if with_backbone else [] + reagents = [] + if with_enzyme: + reagents.append(IndexedReagent("e1", name="BsaI", reagent_type="restriction_enzyme")) + if with_ligase: + reagents.append(IndexedReagent("l1", name="T4_DNA_ligase", reagent_type="ligase")) + return Inventory(backbones=backbones, reagents=reagents) + + +def test_blocked_when_backbone_missing() -> None: + stage = DomesticationStage(inventory=_inventory(with_backbone=False)) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert result.status == StageStatus.BLOCKED + assert result.missing_inputs[0].missing_kind == "backbone" + + +def test_blocked_when_reagents_missing() -> None: + stage = DomesticationStage(inventory=_inventory(with_enzyme=False, with_ligase=False)) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + kinds = {item.missing_kind for item in result.missing_inputs} + assert result.status == StageStatus.BLOCKED + assert "restriction_enzyme" in kinds + assert "ligase" in kinds + + +def test_default_options_block_for_sequence_edit_approval() -> None: + stage = DomesticationStage(inventory=_inventory()) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert result.status == StageStatus.BLOCKED + assert result.required_approvals + + +def test_protocol_mode_requires_explicit_process_or_approval_id() -> None: + options = BuildOptions() + options.domestication.allow_sequence_domestication_edits = True + options.protocol.mode = ProtocolMode.MANUAL + stage = DomesticationStage(inventory=_inventory(), options=options) + blocked = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert blocked.status == StageStatus.BLOCKED + + options.approvals.approved_processes.add("domestication_sequence_edit") + stage2 = DomesticationStage(inventory=_inventory(), options=options) + ok = stage2.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert ok.status == StageStatus.SUCCESS + assert ok.products[0].metadata["insert_identities"] == ["part"] diff --git a/tests/unit/test_core_imports.py b/tests/unit/test_core_imports.py new file mode 100644 index 0000000..8f86fa9 --- /dev/null +++ b/tests/unit/test_core_imports.py @@ -0,0 +1,28 @@ +import sys + + +def test_core_imports_do_not_load_optional_automation_dependencies(): + import buildcompiler + from buildcompiler.adapters.pudu import ( + assembly_route_to_pudu_json, + plating_to_pudu_json, + transformation_to_pudu_json, + ) + from buildcompiler.api import BuildCompiler, BuildOptions + from buildcompiler.execution import FullBuildExecutor + from buildcompiler.reporting import BuildGraph, BuildReport, BuildSummary + + assert buildcompiler + assert BuildCompiler + assert BuildOptions + assert FullBuildExecutor + assert BuildSummary + assert BuildReport + assert BuildGraph + assert assembly_route_to_pudu_json + assert transformation_to_pudu_json + assert plating_to_pudu_json + + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules diff --git a/uv.lock b/uv.lock index 8309d65..ddf3f2e 100644 --- a/uv.lock +++ b/uv.lock @@ -1993,7 +1993,7 @@ wheels = [ [[package]] name = "sbol2build" -version = "0.0b1" +version = "0.0b3" source = { editable = "." } dependencies = [ { name = "biopython", version = "1.81", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" },