Skip to content

Specification Language Module

Parser and interpreter for the specification language.

alienbio.spec_lang

Spec Language Module.

YAML tags, decorators, and Bio class for loading/saving biology specifications. See docs: [[Spec Language]], [[Decorators]], [[Bio]]

Bio

Top-level API for Alien Biology operations.

Bio acts as a "pegboard" holding references to implementation classes. The module singleton bio is used by default; create new instances for sandboxing or customization.

Usage

from alienbio import Bio, bio

bio.fetch(...) # Use the module singleton bio.sim(scenario) # Create simulator using default Simulator class

Customize for sandboxing:

my_bio = Bio() my_bio._simulator_factory = JaxSimulator my_bio.sim(scenario) # Uses JaxSimulator

Configure source roots:

bio.add_source_root("./catalog", module="myproject.catalog")

Create Bio bound to a specific DAT:

sandbox = Bio(dat="experiments/baseline")

ORM Pattern
  • DATs are cached: same DAT name returns the same object
  • First fetch loads DAT into memory; subsequent fetches return cached instance
Source code in src/alienbio/spec_lang/bio.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
class Bio:
    """Top-level API for Alien Biology operations.

    Bio acts as a "pegboard" holding references to implementation classes.
    The module singleton `bio` is used by default; create new instances
    for sandboxing or customization.

    Usage:
        from alienbio import Bio, bio

        bio.fetch(...)        # Use the module singleton
        bio.sim(scenario)     # Create simulator using default Simulator class

        # Customize for sandboxing:
        my_bio = Bio()
        my_bio._simulator_factory = JaxSimulator
        my_bio.sim(scenario)  # Uses JaxSimulator

        # Configure source roots:
        bio.add_source_root("./catalog", module="myproject.catalog")

        # Create Bio bound to a specific DAT:
        sandbox = Bio(dat="experiments/baseline")

    ORM Pattern:
        - DATs are cached: same DAT name returns the same object
        - First fetch loads DAT into memory; subsequent fetches return cached instance
    """

    def __init__(self, *, dat: str | Any | None = None) -> None:
        """Initialize Bio with default implementations.

        Args:
            dat: Optional DAT to bind this Bio to (string path or DAT object)
        """
        from alienbio.bio.simulator import ReferenceSimulatorImpl

        self._simulator_factory: Any = ReferenceSimulatorImpl
        self._source_roots: list[SourceRoot] = []
        self._dat_ref: str | Any | None = dat
        self._dat_object: Any = None
        self._current_dat: Path | None = None

        # Component pegboard attributes
        self._io: Any = None
        self._sim: "Simulator | None" = None
        self._agent: Any = None
        self._chem: Any = None

        # Auto-configure catalog source root
        self._add_catalog_source_root()

    def _add_catalog_source_root(self) -> None:
        """Add the built-in catalog as a source root.

        Finds the catalog directory relative to the alienbio package and
        adds it as a source root for dotted-path resolution.
        """
        import alienbio
        package_dir = Path(alienbio.__file__).parent.parent.parent  # src/alienbio -> src -> project
        catalog_dir = package_dir / "catalog"
        if catalog_dir.exists():
            self._source_roots.append(SourceRoot(catalog_dir, module=None))

    # =========================================================================
    # Component Pegboard
    # =========================================================================

    @property
    def io(self) -> Any:
        """Active IO instance for entity I/O.

        Lazily creates a default IO instance on first access.
        """
        if self._io is None:
            from alienbio.infra.io import IO
            self._io = IO()
        return self._io

    @io.setter
    def io(self, value: Any) -> None:
        self._io = value

    @property
    def sim(self) -> "Simulator | None":
        """Active Simulator instance."""
        return self._sim

    @sim.setter
    def sim(self, value: "Simulator | None") -> None:
        self._sim = value

    @property
    def agent(self) -> Any:
        """Active Agent instance."""
        return self._agent

    @agent.setter
    def agent(self, value: Any) -> None:
        self._agent = value

    @property
    def chem(self) -> Any:
        """Active Chemistry instance."""
        return self._chem

    @chem.setter
    def chem(self, value: Any) -> None:
        self._chem = value

    def create(
        self,
        protocol: type,
        name: str | None = None,
        spec: Any = None,
    ) -> Any:
        """Create component instance via factory.

        Args:
            protocol: Protocol class (Simulator, IO, Agent, Chemistry, etc.)
            name: Implementation name. If None, uses default for protocol.
            spec: Data/configuration for the instance.

        Returns:
            New instance of the specified implementation.

        Raises:
            KeyError: If no implementation found for protocol/name.
        """
        impl_class = _resolve_factory(protocol, name)
        if spec is not None:
            return impl_class(spec)
        return impl_class()

    # =========================================================================
    # Compiled Simulator
    # =========================================================================

    def compile_sim(self, scenario: Any, dt: float = 1.0) -> Any:
        """Create a compiled simulator from a scenario spec.

        Compiles rate expressions (Quoted strings) into efficient callables,
        returning a CompiledSimulator with step/run/action/measure methods.

        Args:
            scenario: ScenarioSpec, dict, or any object with molecules,
                      reactions, initial_state, scope attributes
            dt: Timestep size (default 1.0)

        Returns:
            CompiledSimulator instance
        """
        from .compiled_sim import compile_sim
        return compile_sim(scenario, dt=dt)

    # =========================================================================
    # Source Root Configuration
    # =========================================================================

    def add_source_root(self, path: str | Path, module: str | None = None) -> None:
        """Add a source root for spec resolution.

        Args:
            path: Filesystem path to search for YAML files
            module: Optional Python module prefix for Python global lookups
        """
        expanded_path = Path(path).expanduser()
        self._source_roots.append(SourceRoot(expanded_path, module))

    # =========================================================================
    # Current DAT (cd)
    # =========================================================================

    def cd(self, path: Any = _UNSET) -> Path | None:
        """Get, set, or reset the current working DAT.

        - ``cd()`` — return current DAT path
        - ``cd(path)`` — set current DAT to path
        - ``cd(None)`` — reset (clear current DAT)

        Args:
            path: DAT path to set, None to reset, or omit to get current

        Returns:
            Current DAT path (or None if reset/unset)
        """
        if path is _UNSET:
            return self._current_dat
        elif path is None:
            self._current_dat = None
            return None
        else:
            self._current_dat = Path(path).expanduser().resolve()
            return self._current_dat

    # =========================================================================
    # DAT Accessor
    # =========================================================================

    @property
    def dat(self) -> Any:
        """Get this Bio's bound DAT, creating an anonymous one if needed."""
        if self._dat_object is not None:
            return self._dat_object

        if self._dat_ref is None:
            self._dat_object = {}                             # anonymous DAT
            return self._dat_object

        if isinstance(self._dat_ref, str):
            self._dat_object = self.fetch(self._dat_ref)      # fetch by name
            return self._dat_object

        self._dat_object = self._dat_ref                      # passed directly
        return self._dat_object

    # =========================================================================
    # Cache Management
    # =========================================================================

    @classmethod
    def clear_cache(cls) -> None:
        """Clear the DAT cache."""
        clear_global_cache()

    # =========================================================================
    # Fetch / Store / Expand
    # =========================================================================

    def fetch(
        self, specifier: str, *, raw: bool = False, hydrate: bool = True
    ) -> Any:
        """Fetch a typed object from a specifier path.

        Args:
            specifier: Path like "catalog/scenarios/mutualism" or "mute.mol.energy"
            raw: If True, return raw YAML without processing
            hydrate: If False, resolve tags but don't convert to typed objects

        Returns:
            Processed data (or typed object when hydration implemented)

        Raises:
            FileNotFoundError: If specifier not found
        """
        cache = get_global_cache()

        # Try source root resolution for dotted paths
        if "/" not in specifier and self._source_roots:
            result = self._fetch_from_source_roots(specifier, raw=raw, hydrate=hydrate)
            if result is not None:
                return result

        # Try Python module import for dotted paths (e.g., 'alienbio.bio.Chemistry')
        if "/" not in specifier and "." in specifier:
            result = self._fetch_python_import(specifier)
            if result is not None:
                return result
            if self._source_roots:
                searched = [str(r.path) for r in self._source_roots]
                raise FileNotFoundError(f"'{specifier}' not found in source roots: {searched}")

        # Resolve specifier to path
        resolved = resolve_specifier(specifier, self._source_roots, self._current_dat)

        # Check cache (skip for raw or dig paths)
        if not raw and not resolved.dig_path and resolved.cache_key in cache:
            return cache.get(resolved.cache_key)

        # Load YAML
        content = resolved.path.read_text()
        data = yaml.safe_load(content)

        if data is None:
            return None

        # Raw mode: return unprocessed
        if raw:
            if resolved.dig_path:
                return dig_into(data, resolved.dig_path)
            return data

        # Process and cache
        result = process_and_hydrate(data, resolved.base_dir, hydrate=hydrate)

        if not resolved.dig_path:
            cache.set(resolved.cache_key, result)

        if resolved.dig_path:
            return dig_into(result, resolved.dig_path)

        return result

    def _fetch_from_source_roots(
        self, dotted_path: str, *, raw: bool = False, hydrate: bool = True
    ) -> Any | None:
        """Fetch from source roots using dotted path."""
        for root in self._source_roots:
            result = resolve_dotted_in_source_root(dotted_path, root)
            if result is not None:
                data, base_dir, _ = result
                if raw:
                    return data
                if isinstance(data, dict):
                    return process_and_hydrate(data, base_dir, hydrate=hydrate)
                return data
        return None

    def _fetch_python_import(self, dotted_path: str) -> Any | None:
        """Try to import a Python object by its full dotted path.

        Handles paths like 'alienbio.bio.Chemistry' → imports module,
        returns the attribute.
        """
        import importlib

        parts = dotted_path.rsplit(".", 1)
        if len(parts) != 2:
            return None
        module_path, attr_name = parts
        try:
            module = importlib.import_module(module_path)
            if hasattr(module, attr_name):
                return getattr(module, attr_name)
        except (ImportError, ModuleNotFoundError):
            pass
        return None

    def store(self, specifier: str, obj: Any, *, raw: bool = False) -> None:
        """Store a typed object to a specifier path.

        Dehydration pipeline (inverse of fetch):
        1. Convert typed objects to dicts (via to_dict() if available)
        2. Convert placeholders back to tag form:
           - Evaluable → {"!ev": source}
           - Quoted → {"!_": source}
           - Reference → {"!ref": name}
        3. Write YAML

        Args:
            specifier: Path or file like "output.yaml", "dat_dir/", or "./relative"
            obj: Object to store (dict, typed object, or hydrated data)
            raw: If True, write obj directly without any dehydration
        """
        from .eval import dehydrate

        # Resolve path
        if specifier.startswith("./"):
            if self._current_dat is None:
                raise ValueError("Relative path requires current DAT (use bio.cd() first)")
            path = self._current_dat / specifier[2:]
        else:
            path = Path(specifier)

        # Determine output file: if path has .yaml/.yml suffix, write directly;
        # otherwise treat as DAT directory and write index.yaml
        if path.suffix in ('.yaml', '.yml'):
            spec_file = path
            spec_file.parent.mkdir(parents=True, exist_ok=True)
        else:
            path.mkdir(parents=True, exist_ok=True)
            spec_file = path / "index.yaml"

        # Convert object to dict
        if raw:
            data = obj
        elif isinstance(obj, dict):
            data = dehydrate(obj)
        elif hasattr(obj, 'to_dict'):
            raw_data = obj.to_dict()
            if hasattr(type(obj), '_biotype_name'):
                raw_data["_type"] = type(obj)._biotype_name
            data = dehydrate(raw_data)
        else:
            import dataclasses
            is_biotype = hasattr(type(obj), '_biotype_name')
            if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
                raw_data = dataclasses.asdict(obj)
            else:
                raw_data = {k: v for k, v in vars(obj).items() if not k.startswith('_')}
            if is_biotype:
                raw_data["_type"] = type(obj)._biotype_name
            data = dehydrate(raw_data)

        # Write YAML
        with open(spec_file, "w") as f:
            yaml.dump(data, f, default_flow_style=False)

    def expand(self, specifier: str) -> dict[str, Any]:
        """Expand a spec: resolve includes, refs, defaults without hydrating.

        Args:
            specifier: Path like "catalog/scenarios/mutualism"

        Returns:
            Fully expanded dict with _type fields
        """
        path = Path(specifier)

        if not path.exists():
            raise FileNotFoundError(f"Specifier path not found: {specifier}")

        if path.is_dir():
            spec_file = path / "index.yaml"
            if not spec_file.exists():
                raise FileNotFoundError(f"No index.yaml found in: {specifier}")
        else:
            spec_file = path

        content = spec_file.read_text()
        data = yaml.safe_load(content)

        if data is None:
            return {}

        base_dir = str(spec_file.parent)

        data = resolve_includes(data, base_dir)
        data = transform_typed_keys(data)
        data = resolve_refs(data, data.get("constants", {}))
        data = expand_defaults(data)

        return data

    # =========================================================================
    # Spec Evaluation
    # =========================================================================

    def load_spec(self, specifier: str) -> Any:
        """Load a spec file with placeholders for deferred evaluation.

        Args:
            specifier: Path to spec

        Returns:
            Hydrated spec with placeholders (not yet evaluated)
        """
        path = Path(specifier)

        if not path.exists():
            raise FileNotFoundError(f"Specifier path not found: {specifier}")

        if path.is_dir():
            spec_file = path / "index.yaml"
            if not spec_file.exists():
                raise FileNotFoundError(f"No index.yaml found in: {specifier}")
        else:
            spec_file = path

        content = spec_file.read_text()
        data = yaml.safe_load(content)

        if data is None:
            return None

        return hydrate(data, base_path=str(spec_file.parent))

    def eval_spec(
        self,
        spec: Any,
        *,
        seed: int | None = None,
        bindings: dict[str, Any] | None = None,
        ctx: EvalContext | None = None,
    ) -> Any:
        """Evaluate a hydrated spec, resolving all placeholders.

        Args:
            spec: Hydrated spec from load_spec()
            seed: Random seed for reproducibility
            bindings: Variables available to expressions
            ctx: Full evaluation context (overrides seed/bindings)

        Returns:
            Fully evaluated spec with concrete values
        """
        if ctx is None:
            ctx = make_context(seed=seed, bindings=bindings)
        return eval_node(spec, ctx)

    # =========================================================================
    # Build / Run / Sim
    # =========================================================================

    def build(
        self,
        spec: str | dict[str, Any],
        seed: int = 0,
        registry: Any = None,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """Build a scenario from a spec.

        Args:
            spec: Spec dict or specifier string
            seed: Random seed for reproducibility
            registry: Template registry
            params: Parameter overrides

        Returns:
            Scenario with visible and ground truth data
        """
        from alienbio.build import instantiate as build_instantiate

        if isinstance(spec, str):
            spec = self.fetch(spec, raw=True)

        return build_instantiate(spec, seed=seed, registry=registry, params=params)  # type: ignore[arg-type]

    def run(
        self,
        target: str | dict[str, Any],
        seed: int = 0,
        registry: Any = None,
        params: dict[str, Any] | None = None,
        steps: int | None = None,
        dt: float | None = None,
    ) -> SimulationResult:
        """Run a target: build if needed, then execute simulation.

        This is the main entry point for M3.1 Scenario Execution.

        Pipeline:
        1. If target is a string or dict spec, build it into a Scenario
        2. Extract sim settings (steps, dt) from scenario or use defaults
        3. Build Chemistry from scenario ground truth
        4. Initialize State from scenario regions/containers
        5. Create simulator and run for N steps
        6. Return SimulationResult with timeline

        Args:
            target: Specifier string, dict spec, Scenario, or DAT
            seed: Random seed for reproducibility
            registry: Template registry for building
            params: Parameter overrides for building
            steps: Override number of simulation steps (default: from scenario or 100)
            dt: Override time step (default: from scenario or 1.0)

        Returns:
            SimulationResult with timeline of states
        """
        from alienbio.protocols import Scenario
        from alienbio.bio.chemistry import ChemistryImpl
        from alienbio.bio.state import StateImpl

        # Build scenario if needed
        if isinstance(target, str):
            scenario = self.build(target, seed=seed, registry=registry, params=params)
        elif isinstance(target, dict):
            # Check if it's a raw spec or already a scenario-like dict
            if "_ground_truth_" in target or "molecules" in target:
                scenario = target  # Already scenario-like
            else:
                scenario = self.build(target, seed=seed, registry=registry, params=params)
        else:
            scenario = target

        # Extract scenario data depending on type
        if isinstance(scenario, Scenario):
            ground_truth = scenario._ground_truth_
            regions = scenario.regions
            metadata = scenario._metadata_
            scenario_name = metadata.get("name", "scenario")
            scenario_seed = scenario._seed
        else:
            # Dict-based scenario
            ground_truth = scenario.get("_ground_truth_", scenario)
            regions = scenario.get("regions", [])
            metadata = scenario.get("_metadata_", {})
            scenario_name = metadata.get("name", scenario.get("name", "scenario"))
            scenario_seed = scenario.get("_seed", seed)

        # Extract sim settings (with overrides)
        sim_config = metadata.get("sim", {})
        effective_steps = steps if steps is not None else sim_config.get("steps", 100)
        effective_dt = dt if dt is not None else sim_config.get("dt", 1.0)

        # Build Chemistry from ground truth
        chemistry_data = {
            "molecules": ground_truth.get("molecules", {}),
            "reactions": ground_truth.get("reactions", {}),
        }
        chemistry = ChemistryImpl.hydrate(chemistry_data, local_name=scenario_name)

        # Initialize State from regions/containers
        initial_concentrations = self._extract_initial_state(regions, ground_truth)
        state = StateImpl(chemistry, initial=initial_concentrations)

        # Create simulator and run
        sim = self._simulator_factory(chemistry, dt=effective_dt)  # type: ignore[call-arg]
        timeline = sim.run(state, steps=effective_steps)  # type: ignore[arg-type]

        return SimulationResult(
            timeline=timeline,
            final_state=timeline[-1] if timeline else None,
            steps=effective_steps,
            dt=effective_dt,
            seed=scenario_seed,
            scenario_name=scenario_name,
        )

    def _extract_initial_state(
        self,
        regions: list,
        ground_truth: dict[str, Any],
    ) -> dict[str, float]:
        """Extract initial concentrations from regions and ground truth.

        For M3.1, this provides a simple initial state extraction.
        Future milestones will handle more complex region-based initialization.

        Args:
            regions: List of Region objects with organisms and substrates
            ground_truth: Ground truth data with molecules

        Returns:
            Dict of molecule name -> initial concentration
        """
        initial: dict[str, float] = {}

        # First, initialize all molecules to 0
        for mol_name in ground_truth.get("molecules", {}):
            initial[mol_name] = 0.0

        # Extract initial concentrations from regions
        if regions:
            from alienbio.protocols import Region
            for region in regions:
                if isinstance(region, Region):
                    # Add substrate concentrations
                    for substrate, conc in region.substrates.items():
                        # Map substrate names to molecule names
                        for mol_name in initial:
                            if substrate in mol_name or mol_name.endswith(f".{substrate}"):
                                initial[mol_name] = conc
                elif isinstance(region, dict):
                    for substrate, conc in region.get("substrates", {}).items():
                        for mol_name in initial:
                            if substrate in mol_name or mol_name.endswith(f".{substrate}"):
                                initial[mol_name] = conc

        # If no regions with substrates, set some default non-zero values
        # This ensures the simulation actually does something
        if all(v == 0.0 for v in initial.values()):
            # Set first molecule to 1.0 as a default starting point
            for mol_name in initial:
                initial[mol_name] = 1.0
                break

        return initial

io property writable

Active IO instance for entity I/O.

Lazily creates a default IO instance on first access.

sim property writable

Active Simulator instance.

agent property writable

Active Agent instance.

chem property writable

Active Chemistry instance.

dat property

Get this Bio's bound DAT, creating an anonymous one if needed.

__init__(*, dat=None)

Initialize Bio with default implementations.

Parameters:

Name Type Description Default
dat str | Any | None

Optional DAT to bind this Bio to (string path or DAT object)

None
Source code in src/alienbio/spec_lang/bio.py
def __init__(self, *, dat: str | Any | None = None) -> None:
    """Initialize Bio with default implementations.

    Args:
        dat: Optional DAT to bind this Bio to (string path or DAT object)
    """
    from alienbio.bio.simulator import ReferenceSimulatorImpl

    self._simulator_factory: Any = ReferenceSimulatorImpl
    self._source_roots: list[SourceRoot] = []
    self._dat_ref: str | Any | None = dat
    self._dat_object: Any = None
    self._current_dat: Path | None = None

    # Component pegboard attributes
    self._io: Any = None
    self._sim: "Simulator | None" = None
    self._agent: Any = None
    self._chem: Any = None

    # Auto-configure catalog source root
    self._add_catalog_source_root()

create(protocol, name=None, spec=None)

Create component instance via factory.

Parameters:

Name Type Description Default
protocol type

Protocol class (Simulator, IO, Agent, Chemistry, etc.)

required
name str | None

Implementation name. If None, uses default for protocol.

None
spec Any

Data/configuration for the instance.

None

Returns:

Type Description
Any

New instance of the specified implementation.

Raises:

Type Description
KeyError

If no implementation found for protocol/name.

Source code in src/alienbio/spec_lang/bio.py
def create(
    self,
    protocol: type,
    name: str | None = None,
    spec: Any = None,
) -> Any:
    """Create component instance via factory.

    Args:
        protocol: Protocol class (Simulator, IO, Agent, Chemistry, etc.)
        name: Implementation name. If None, uses default for protocol.
        spec: Data/configuration for the instance.

    Returns:
        New instance of the specified implementation.

    Raises:
        KeyError: If no implementation found for protocol/name.
    """
    impl_class = _resolve_factory(protocol, name)
    if spec is not None:
        return impl_class(spec)
    return impl_class()

compile_sim(scenario, dt=1.0)

Create a compiled simulator from a scenario spec.

Compiles rate expressions (Quoted strings) into efficient callables, returning a CompiledSimulator with step/run/action/measure methods.

Parameters:

Name Type Description Default
scenario Any

ScenarioSpec, dict, or any object with molecules, reactions, initial_state, scope attributes

required
dt float

Timestep size (default 1.0)

1.0

Returns:

Type Description
Any

CompiledSimulator instance

Source code in src/alienbio/spec_lang/bio.py
def compile_sim(self, scenario: Any, dt: float = 1.0) -> Any:
    """Create a compiled simulator from a scenario spec.

    Compiles rate expressions (Quoted strings) into efficient callables,
    returning a CompiledSimulator with step/run/action/measure methods.

    Args:
        scenario: ScenarioSpec, dict, or any object with molecules,
                  reactions, initial_state, scope attributes
        dt: Timestep size (default 1.0)

    Returns:
        CompiledSimulator instance
    """
    from .compiled_sim import compile_sim
    return compile_sim(scenario, dt=dt)

add_source_root(path, module=None)

Add a source root for spec resolution.

Parameters:

Name Type Description Default
path str | Path

Filesystem path to search for YAML files

required
module str | None

Optional Python module prefix for Python global lookups

None
Source code in src/alienbio/spec_lang/bio.py
def add_source_root(self, path: str | Path, module: str | None = None) -> None:
    """Add a source root for spec resolution.

    Args:
        path: Filesystem path to search for YAML files
        module: Optional Python module prefix for Python global lookups
    """
    expanded_path = Path(path).expanduser()
    self._source_roots.append(SourceRoot(expanded_path, module))

cd(path=_UNSET)

Get, set, or reset the current working DAT.

  • cd() — return current DAT path
  • cd(path) — set current DAT to path
  • cd(None) — reset (clear current DAT)

Parameters:

Name Type Description Default
path Any

DAT path to set, None to reset, or omit to get current

_UNSET

Returns:

Type Description
Path | None

Current DAT path (or None if reset/unset)

Source code in src/alienbio/spec_lang/bio.py
def cd(self, path: Any = _UNSET) -> Path | None:
    """Get, set, or reset the current working DAT.

    - ``cd()`` — return current DAT path
    - ``cd(path)`` — set current DAT to path
    - ``cd(None)`` — reset (clear current DAT)

    Args:
        path: DAT path to set, None to reset, or omit to get current

    Returns:
        Current DAT path (or None if reset/unset)
    """
    if path is _UNSET:
        return self._current_dat
    elif path is None:
        self._current_dat = None
        return None
    else:
        self._current_dat = Path(path).expanduser().resolve()
        return self._current_dat

clear_cache() classmethod

Clear the DAT cache.

Source code in src/alienbio/spec_lang/bio.py
@classmethod
def clear_cache(cls) -> None:
    """Clear the DAT cache."""
    clear_global_cache()

fetch(specifier, *, raw=False, hydrate=True)

Fetch a typed object from a specifier path.

Parameters:

Name Type Description Default
specifier str

Path like "catalog/scenarios/mutualism" or "mute.mol.energy"

required
raw bool

If True, return raw YAML without processing

False
hydrate bool

If False, resolve tags but don't convert to typed objects

True

Returns:

Type Description
Any

Processed data (or typed object when hydration implemented)

Raises:

Type Description
FileNotFoundError

If specifier not found

Source code in src/alienbio/spec_lang/bio.py
def fetch(
    self, specifier: str, *, raw: bool = False, hydrate: bool = True
) -> Any:
    """Fetch a typed object from a specifier path.

    Args:
        specifier: Path like "catalog/scenarios/mutualism" or "mute.mol.energy"
        raw: If True, return raw YAML without processing
        hydrate: If False, resolve tags but don't convert to typed objects

    Returns:
        Processed data (or typed object when hydration implemented)

    Raises:
        FileNotFoundError: If specifier not found
    """
    cache = get_global_cache()

    # Try source root resolution for dotted paths
    if "/" not in specifier and self._source_roots:
        result = self._fetch_from_source_roots(specifier, raw=raw, hydrate=hydrate)
        if result is not None:
            return result

    # Try Python module import for dotted paths (e.g., 'alienbio.bio.Chemistry')
    if "/" not in specifier and "." in specifier:
        result = self._fetch_python_import(specifier)
        if result is not None:
            return result
        if self._source_roots:
            searched = [str(r.path) for r in self._source_roots]
            raise FileNotFoundError(f"'{specifier}' not found in source roots: {searched}")

    # Resolve specifier to path
    resolved = resolve_specifier(specifier, self._source_roots, self._current_dat)

    # Check cache (skip for raw or dig paths)
    if not raw and not resolved.dig_path and resolved.cache_key in cache:
        return cache.get(resolved.cache_key)

    # Load YAML
    content = resolved.path.read_text()
    data = yaml.safe_load(content)

    if data is None:
        return None

    # Raw mode: return unprocessed
    if raw:
        if resolved.dig_path:
            return dig_into(data, resolved.dig_path)
        return data

    # Process and cache
    result = process_and_hydrate(data, resolved.base_dir, hydrate=hydrate)

    if not resolved.dig_path:
        cache.set(resolved.cache_key, result)

    if resolved.dig_path:
        return dig_into(result, resolved.dig_path)

    return result

store(specifier, obj, *, raw=False)

Store a typed object to a specifier path.

Dehydration pipeline (inverse of fetch): 1. Convert typed objects to dicts (via to_dict() if available) 2. Convert placeholders back to tag form: - Evaluable → {"!ev": source} - Quoted → {"!_": source} - Reference → {"!ref": name} 3. Write YAML

Parameters:

Name Type Description Default
specifier str

Path or file like "output.yaml", "dat_dir/", or "./relative"

required
obj Any

Object to store (dict, typed object, or hydrated data)

required
raw bool

If True, write obj directly without any dehydration

False
Source code in src/alienbio/spec_lang/bio.py
def store(self, specifier: str, obj: Any, *, raw: bool = False) -> None:
    """Store a typed object to a specifier path.

    Dehydration pipeline (inverse of fetch):
    1. Convert typed objects to dicts (via to_dict() if available)
    2. Convert placeholders back to tag form:
       - Evaluable → {"!ev": source}
       - Quoted → {"!_": source}
       - Reference → {"!ref": name}
    3. Write YAML

    Args:
        specifier: Path or file like "output.yaml", "dat_dir/", or "./relative"
        obj: Object to store (dict, typed object, or hydrated data)
        raw: If True, write obj directly without any dehydration
    """
    from .eval import dehydrate

    # Resolve path
    if specifier.startswith("./"):
        if self._current_dat is None:
            raise ValueError("Relative path requires current DAT (use bio.cd() first)")
        path = self._current_dat / specifier[2:]
    else:
        path = Path(specifier)

    # Determine output file: if path has .yaml/.yml suffix, write directly;
    # otherwise treat as DAT directory and write index.yaml
    if path.suffix in ('.yaml', '.yml'):
        spec_file = path
        spec_file.parent.mkdir(parents=True, exist_ok=True)
    else:
        path.mkdir(parents=True, exist_ok=True)
        spec_file = path / "index.yaml"

    # Convert object to dict
    if raw:
        data = obj
    elif isinstance(obj, dict):
        data = dehydrate(obj)
    elif hasattr(obj, 'to_dict'):
        raw_data = obj.to_dict()
        if hasattr(type(obj), '_biotype_name'):
            raw_data["_type"] = type(obj)._biotype_name
        data = dehydrate(raw_data)
    else:
        import dataclasses
        is_biotype = hasattr(type(obj), '_biotype_name')
        if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
            raw_data = dataclasses.asdict(obj)
        else:
            raw_data = {k: v for k, v in vars(obj).items() if not k.startswith('_')}
        if is_biotype:
            raw_data["_type"] = type(obj)._biotype_name
        data = dehydrate(raw_data)

    # Write YAML
    with open(spec_file, "w") as f:
        yaml.dump(data, f, default_flow_style=False)

expand(specifier)

Expand a spec: resolve includes, refs, defaults without hydrating.

Parameters:

Name Type Description Default
specifier str

Path like "catalog/scenarios/mutualism"

required

Returns:

Type Description
dict[str, Any]

Fully expanded dict with _type fields

Source code in src/alienbio/spec_lang/bio.py
def expand(self, specifier: str) -> dict[str, Any]:
    """Expand a spec: resolve includes, refs, defaults without hydrating.

    Args:
        specifier: Path like "catalog/scenarios/mutualism"

    Returns:
        Fully expanded dict with _type fields
    """
    path = Path(specifier)

    if not path.exists():
        raise FileNotFoundError(f"Specifier path not found: {specifier}")

    if path.is_dir():
        spec_file = path / "index.yaml"
        if not spec_file.exists():
            raise FileNotFoundError(f"No index.yaml found in: {specifier}")
    else:
        spec_file = path

    content = spec_file.read_text()
    data = yaml.safe_load(content)

    if data is None:
        return {}

    base_dir = str(spec_file.parent)

    data = resolve_includes(data, base_dir)
    data = transform_typed_keys(data)
    data = resolve_refs(data, data.get("constants", {}))
    data = expand_defaults(data)

    return data

load_spec(specifier)

Load a spec file with placeholders for deferred evaluation.

Parameters:

Name Type Description Default
specifier str

Path to spec

required

Returns:

Type Description
Any

Hydrated spec with placeholders (not yet evaluated)

Source code in src/alienbio/spec_lang/bio.py
def load_spec(self, specifier: str) -> Any:
    """Load a spec file with placeholders for deferred evaluation.

    Args:
        specifier: Path to spec

    Returns:
        Hydrated spec with placeholders (not yet evaluated)
    """
    path = Path(specifier)

    if not path.exists():
        raise FileNotFoundError(f"Specifier path not found: {specifier}")

    if path.is_dir():
        spec_file = path / "index.yaml"
        if not spec_file.exists():
            raise FileNotFoundError(f"No index.yaml found in: {specifier}")
    else:
        spec_file = path

    content = spec_file.read_text()
    data = yaml.safe_load(content)

    if data is None:
        return None

    return hydrate(data, base_path=str(spec_file.parent))

eval_spec(spec, *, seed=None, bindings=None, ctx=None)

Evaluate a hydrated spec, resolving all placeholders.

Parameters:

Name Type Description Default
spec Any

Hydrated spec from load_spec()

required
seed int | None

Random seed for reproducibility

None
bindings dict[str, Any] | None

Variables available to expressions

None
ctx EvalContext | None

Full evaluation context (overrides seed/bindings)

None

Returns:

Type Description
Any

Fully evaluated spec with concrete values

Source code in src/alienbio/spec_lang/bio.py
def eval_spec(
    self,
    spec: Any,
    *,
    seed: int | None = None,
    bindings: dict[str, Any] | None = None,
    ctx: EvalContext | None = None,
) -> Any:
    """Evaluate a hydrated spec, resolving all placeholders.

    Args:
        spec: Hydrated spec from load_spec()
        seed: Random seed for reproducibility
        bindings: Variables available to expressions
        ctx: Full evaluation context (overrides seed/bindings)

    Returns:
        Fully evaluated spec with concrete values
    """
    if ctx is None:
        ctx = make_context(seed=seed, bindings=bindings)
    return eval_node(spec, ctx)

build(spec, seed=0, registry=None, params=None)

Build a scenario from a spec.

Parameters:

Name Type Description Default
spec str | dict[str, Any]

Spec dict or specifier string

required
seed int

Random seed for reproducibility

0
registry Any

Template registry

None
params dict[str, Any] | None

Parameter overrides

None

Returns:

Type Description
Any

Scenario with visible and ground truth data

Source code in src/alienbio/spec_lang/bio.py
def build(
    self,
    spec: str | dict[str, Any],
    seed: int = 0,
    registry: Any = None,
    params: dict[str, Any] | None = None,
) -> Any:
    """Build a scenario from a spec.

    Args:
        spec: Spec dict or specifier string
        seed: Random seed for reproducibility
        registry: Template registry
        params: Parameter overrides

    Returns:
        Scenario with visible and ground truth data
    """
    from alienbio.build import instantiate as build_instantiate

    if isinstance(spec, str):
        spec = self.fetch(spec, raw=True)

    return build_instantiate(spec, seed=seed, registry=registry, params=params)  # type: ignore[arg-type]

run(target, seed=0, registry=None, params=None, steps=None, dt=None)

Run a target: build if needed, then execute simulation.

This is the main entry point for M3.1 Scenario Execution.

Pipeline: 1. If target is a string or dict spec, build it into a Scenario 2. Extract sim settings (steps, dt) from scenario or use defaults 3. Build Chemistry from scenario ground truth 4. Initialize State from scenario regions/containers 5. Create simulator and run for N steps 6. Return SimulationResult with timeline

Parameters:

Name Type Description Default
target str | dict[str, Any]

Specifier string, dict spec, Scenario, or DAT

required
seed int

Random seed for reproducibility

0
registry Any

Template registry for building

None
params dict[str, Any] | None

Parameter overrides for building

None
steps int | None

Override number of simulation steps (default: from scenario or 100)

None
dt float | None

Override time step (default: from scenario or 1.0)

None

Returns:

Type Description
SimulationResult

SimulationResult with timeline of states

Source code in src/alienbio/spec_lang/bio.py
def run(
    self,
    target: str | dict[str, Any],
    seed: int = 0,
    registry: Any = None,
    params: dict[str, Any] | None = None,
    steps: int | None = None,
    dt: float | None = None,
) -> SimulationResult:
    """Run a target: build if needed, then execute simulation.

    This is the main entry point for M3.1 Scenario Execution.

    Pipeline:
    1. If target is a string or dict spec, build it into a Scenario
    2. Extract sim settings (steps, dt) from scenario or use defaults
    3. Build Chemistry from scenario ground truth
    4. Initialize State from scenario regions/containers
    5. Create simulator and run for N steps
    6. Return SimulationResult with timeline

    Args:
        target: Specifier string, dict spec, Scenario, or DAT
        seed: Random seed for reproducibility
        registry: Template registry for building
        params: Parameter overrides for building
        steps: Override number of simulation steps (default: from scenario or 100)
        dt: Override time step (default: from scenario or 1.0)

    Returns:
        SimulationResult with timeline of states
    """
    from alienbio.protocols import Scenario
    from alienbio.bio.chemistry import ChemistryImpl
    from alienbio.bio.state import StateImpl

    # Build scenario if needed
    if isinstance(target, str):
        scenario = self.build(target, seed=seed, registry=registry, params=params)
    elif isinstance(target, dict):
        # Check if it's a raw spec or already a scenario-like dict
        if "_ground_truth_" in target or "molecules" in target:
            scenario = target  # Already scenario-like
        else:
            scenario = self.build(target, seed=seed, registry=registry, params=params)
    else:
        scenario = target

    # Extract scenario data depending on type
    if isinstance(scenario, Scenario):
        ground_truth = scenario._ground_truth_
        regions = scenario.regions
        metadata = scenario._metadata_
        scenario_name = metadata.get("name", "scenario")
        scenario_seed = scenario._seed
    else:
        # Dict-based scenario
        ground_truth = scenario.get("_ground_truth_", scenario)
        regions = scenario.get("regions", [])
        metadata = scenario.get("_metadata_", {})
        scenario_name = metadata.get("name", scenario.get("name", "scenario"))
        scenario_seed = scenario.get("_seed", seed)

    # Extract sim settings (with overrides)
    sim_config = metadata.get("sim", {})
    effective_steps = steps if steps is not None else sim_config.get("steps", 100)
    effective_dt = dt if dt is not None else sim_config.get("dt", 1.0)

    # Build Chemistry from ground truth
    chemistry_data = {
        "molecules": ground_truth.get("molecules", {}),
        "reactions": ground_truth.get("reactions", {}),
    }
    chemistry = ChemistryImpl.hydrate(chemistry_data, local_name=scenario_name)

    # Initialize State from regions/containers
    initial_concentrations = self._extract_initial_state(regions, ground_truth)
    state = StateImpl(chemistry, initial=initial_concentrations)

    # Create simulator and run
    sim = self._simulator_factory(chemistry, dt=effective_dt)  # type: ignore[call-arg]
    timeline = sim.run(state, steps=effective_steps)  # type: ignore[arg-type]

    return SimulationResult(
        timeline=timeline,
        final_state=timeline[-1] if timeline else None,
        steps=effective_steps,
        dt=effective_dt,
        seed=scenario_seed,
        scenario_name=scenario_name,
    )

SimulationResult dataclass

Result of running a simulation.

Contains the timeline of states and metadata about the run.

Attributes:

Name Type Description
timeline List[Any]

List of states from simulation (StateImpl or dict)

final_state Any

The final state after simulation

steps int

Number of steps executed

dt float

Time step used

seed int

Random seed used for the run

scenario_name str

Name of the scenario that was run

Source code in src/alienbio/spec_lang/bio.py
@dataclass
class SimulationResult:
    """Result of running a simulation.

    Contains the timeline of states and metadata about the run.

    Attributes:
        timeline: List of states from simulation (StateImpl or dict)
        final_state: The final state after simulation
        steps: Number of steps executed
        dt: Time step used
        seed: Random seed used for the run
        scenario_name: Name of the scenario that was run
    """
    timeline: List[Any] = field(default_factory=list)
    final_state: Any = None
    steps: int = 0
    dt: float = 1.0
    seed: int = 0
    scenario_name: str = ""

    @property
    def final(self) -> dict[str, float]:
        """Return the final state as a dict of concentrations.

        Compatible with scoring functions that expect a dict.
        """
        if self.final_state is None:
            return {}
        if hasattr(self.final_state, 'items'):
            # StateImpl or dict
            return dict(self.final_state.items())
        return {}

    def __len__(self) -> int:
        """Return number of states in timeline."""
        return len(self.timeline)

final property

Return the final state as a dict of concentrations.

Compatible with scoring functions that expect a dict.

__len__()

Return number of states in timeline.

Source code in src/alienbio/spec_lang/bio.py
def __len__(self) -> int:
    """Return number of states in timeline."""
    return len(self.timeline)

CompiledSimulator

Simulator created from a scenario spec with compiled rate expressions.

Rate expressions are compiled once at construction time. The simulator operates on dict-based state (molecule name -> concentration).

Source code in src/alienbio/spec_lang/compiled_sim.py
class CompiledSimulator:
    """Simulator created from a scenario spec with compiled rate expressions.

    Rate expressions are compiled once at construction time. The simulator
    operates on dict-based state (molecule name -> concentration).
    """

    def __init__(self, scenario: ScenarioSpec, dt: float = 1.0) -> None:
        self._scenario = scenario
        self._dt = dt
        self._reactions = self._compile_reactions()
        self._internal_state: dict[str, float] | None = None

    def _compile_reactions(self) -> list[_CompiledReaction]:
        """Compile all rate expressions in the scenario."""
        compiled = []
        for name, rxn in self._scenario.reactions.items():
            source = rxn["rate"]
            if isinstance(source, Quoted):
                source = source.source
            elif isinstance(source, (int, float)):
                source = str(source)
            rate_fn = compile_rate_expression(str(source), self._scenario.scope)
            compiled.append(_CompiledReaction(
                name=name,
                substrates=list(rxn.get("substrates", [])),
                products=list(rxn.get("products", [])),
                rate_fn=rate_fn,
            ))
        return compiled

    def initial_state(self) -> dict[str, float]:
        """Return a fresh copy of the initial state."""
        state = dict(self._scenario.initial_state)
        self._internal_state = dict(state)
        return state

    def step(self, state: dict[str, float]) -> dict[str, float]:
        """Advance state by one timestep using Euler integration."""
        new_state = dict(state)
        for rxn in self._reactions:
            rate_state = _build_rate_state(rxn, state)
            rate = rxn.rate_fn(rate_state) * self._dt
            for mol in rxn.substrates:
                new_state[mol] = max(0.0, new_state[mol] - rate)
            for mol in rxn.products:
                new_state[mol] = new_state[mol] + rate
        self._internal_state = dict(new_state)
        return new_state

    def run(
        self, state: dict[str, float], steps: int
    ) -> list[dict[str, float]]:
        """Run simulation for N steps, returning full history.

        Returns:
            List of length steps+1 (initial state + N stepped states)
        """
        history = [dict(state)]
        current = state
        for _ in range(steps):
            current = self.step(current)
            history.append(dict(current))
        return history

    def action(self, name: str, *args: Any) -> None:
        """Execute a named action on the internal state.

        Supported actions:
            add_feedstock(molecule, amount) — increase concentration
        """
        if self._internal_state is None:
            return
        if name == "add_feedstock" and len(args) >= 2:
            molecule, amount = args[0], float(args[1])
            if molecule in self._internal_state:
                self._internal_state[molecule] += amount

    def measure(self, name: str, *args: Any) -> float:
        """Take a named measurement from the internal state."""
        if self._internal_state is None:
            return 0.0
        if name == "concentration" and len(args) >= 1:
            return self._internal_state.get(str(args[0]), 0.0)
        return 0.0

initial_state()

Return a fresh copy of the initial state.

Source code in src/alienbio/spec_lang/compiled_sim.py
def initial_state(self) -> dict[str, float]:
    """Return a fresh copy of the initial state."""
    state = dict(self._scenario.initial_state)
    self._internal_state = dict(state)
    return state

step(state)

Advance state by one timestep using Euler integration.

Source code in src/alienbio/spec_lang/compiled_sim.py
def step(self, state: dict[str, float]) -> dict[str, float]:
    """Advance state by one timestep using Euler integration."""
    new_state = dict(state)
    for rxn in self._reactions:
        rate_state = _build_rate_state(rxn, state)
        rate = rxn.rate_fn(rate_state) * self._dt
        for mol in rxn.substrates:
            new_state[mol] = max(0.0, new_state[mol] - rate)
        for mol in rxn.products:
            new_state[mol] = new_state[mol] + rate
    self._internal_state = dict(new_state)
    return new_state

run(state, steps)

Run simulation for N steps, returning full history.

Returns:

Type Description
list[dict[str, float]]

List of length steps+1 (initial state + N stepped states)

Source code in src/alienbio/spec_lang/compiled_sim.py
def run(
    self, state: dict[str, float], steps: int
) -> list[dict[str, float]]:
    """Run simulation for N steps, returning full history.

    Returns:
        List of length steps+1 (initial state + N stepped states)
    """
    history = [dict(state)]
    current = state
    for _ in range(steps):
        current = self.step(current)
        history.append(dict(current))
    return history

action(name, *args)

Execute a named action on the internal state.

Supported actions

add_feedstock(molecule, amount) — increase concentration

Source code in src/alienbio/spec_lang/compiled_sim.py
def action(self, name: str, *args: Any) -> None:
    """Execute a named action on the internal state.

    Supported actions:
        add_feedstock(molecule, amount) — increase concentration
    """
    if self._internal_state is None:
        return
    if name == "add_feedstock" and len(args) >= 2:
        molecule, amount = args[0], float(args[1])
        if molecule in self._internal_state:
            self._internal_state[molecule] += amount

measure(name, *args)

Take a named measurement from the internal state.

Source code in src/alienbio/spec_lang/compiled_sim.py
def measure(self, name: str, *args: Any) -> float:
    """Take a named measurement from the internal state."""
    if self._internal_state is None:
        return 0.0
    if name == "concentration" and len(args) >= 1:
        return self._internal_state.get(str(args[0]), 0.0)
    return 0.0

ScenarioSpec dataclass

Lightweight scenario specification for compiled simulation.

Attributes:

Name Type Description
name str

Scenario identifier

molecules dict[str, Any]

Molecule names to properties

reactions dict[str, Any]

Reaction name to dict with substrates, products, rate

initial_state dict[str, float]

Molecule name to initial concentration

scope dict[str, Any]

Named constants for rate compilation

Source code in src/alienbio/spec_lang/compiled_sim.py
@dataclass
class ScenarioSpec:
    """Lightweight scenario specification for compiled simulation.

    Attributes:
        name: Scenario identifier
        molecules: Molecule names to properties
        reactions: Reaction name to dict with substrates, products, rate
        initial_state: Molecule name to initial concentration
        scope: Named constants for rate compilation
    """
    name: str
    molecules: dict[str, Any]
    reactions: dict[str, Any]
    initial_state: dict[str, float]
    scope: dict[str, Any] = field(default_factory=dict)

Include

Placeholder for !include tag - file to include.

Resolved during hydration (phase 1).

Source code in src/alienbio/spec_lang/tags.py
class Include:
    """Placeholder for !include tag - file to include.

    Resolved during hydration (phase 1).
    """

    def __init__(self, path: str):
        self.path = path

    def __repr__(self) -> str:
        return f"Include({self.path!r})"

    def load(self, base_dir: str | None = None, _seen: set[str] | None = None) -> Any:
        """Load the included file.

        Args:
            base_dir: Base directory for relative paths
            _seen: Internal set tracking files in current include chain

        Returns:
            File contents (string for .md, parsed for .yaml, executed for .py)

        Raises:
            FileNotFoundError: If file doesn't exist
            RecursionError: If circular include detected
        """
        # Resolve file path
        if Path(self.path).is_absolute():
            file_path = Path(self.path)
        elif base_dir:
            file_path = Path(base_dir) / self.path
        else:
            file_path = Path(self.path)

        file_path = file_path.resolve()

        if not file_path.exists():
            raise FileNotFoundError(f"Include file not found: {file_path}")

        # Check for circular includes
        file_key = str(file_path)
        if _seen is None:
            _seen = set()

        if file_key in _seen:
            raise RecursionError(f"Circular include detected: {file_key}")

        _seen = _seen | {file_key}  # Create new set to avoid cross-branch pollution

        # Load based on file extension
        suffix = file_path.suffix.lower()

        if suffix == ".md":
            return file_path.read_text()

        elif suffix in (".yaml", ".yml"):
            content = file_path.read_text()
            data = yaml.safe_load(content)
            # Recursively resolve any Includes in the loaded data
            return self._resolve_includes(data, str(file_path.parent), _seen)

        elif suffix == ".py":
            # Execute Python file to register decorators
            code = file_path.read_text()
            exec(compile(code, str(file_path), "exec"), {"__name__": "__main__"})
            return None

        else:
            # Default: return raw text
            return file_path.read_text()

    def _resolve_includes(
        self, data: Any, base_dir: str, _seen: set[str]
    ) -> Any:
        """Recursively resolve Includes in loaded data."""
        if isinstance(data, Include):
            return data.load(base_dir, _seen)
        elif isinstance(data, dict):
            return {k: self._resolve_includes(v, base_dir, _seen) for k, v in data.items()}
        elif isinstance(data, list):
            return [self._resolve_includes(item, base_dir, _seen) for item in data]
        else:
            return data

load(base_dir=None, _seen=None)

Load the included file.

Parameters:

Name Type Description Default
base_dir str | None

Base directory for relative paths

None
_seen set[str] | None

Internal set tracking files in current include chain

None

Returns:

Type Description
Any

File contents (string for .md, parsed for .yaml, executed for .py)

Raises:

Type Description
FileNotFoundError

If file doesn't exist

RecursionError

If circular include detected

Source code in src/alienbio/spec_lang/tags.py
def load(self, base_dir: str | None = None, _seen: set[str] | None = None) -> Any:
    """Load the included file.

    Args:
        base_dir: Base directory for relative paths
        _seen: Internal set tracking files in current include chain

    Returns:
        File contents (string for .md, parsed for .yaml, executed for .py)

    Raises:
        FileNotFoundError: If file doesn't exist
        RecursionError: If circular include detected
    """
    # Resolve file path
    if Path(self.path).is_absolute():
        file_path = Path(self.path)
    elif base_dir:
        file_path = Path(base_dir) / self.path
    else:
        file_path = Path(self.path)

    file_path = file_path.resolve()

    if not file_path.exists():
        raise FileNotFoundError(f"Include file not found: {file_path}")

    # Check for circular includes
    file_key = str(file_path)
    if _seen is None:
        _seen = set()

    if file_key in _seen:
        raise RecursionError(f"Circular include detected: {file_key}")

    _seen = _seen | {file_key}  # Create new set to avoid cross-branch pollution

    # Load based on file extension
    suffix = file_path.suffix.lower()

    if suffix == ".md":
        return file_path.read_text()

    elif suffix in (".yaml", ".yml"):
        content = file_path.read_text()
        data = yaml.safe_load(content)
        # Recursively resolve any Includes in the loaded data
        return self._resolve_includes(data, str(file_path.parent), _seen)

    elif suffix == ".py":
        # Execute Python file to register decorators
        code = file_path.read_text()
        exec(compile(code, str(file_path), "exec"), {"__name__": "__main__"})
        return None

    else:
        # Default: return raw text
        return file_path.read_text()

Evaluable dataclass

Placeholder for !ev expressions - evaluated at instantiation time.

Created during hydration when a !ev tag is encountered. Evaluated by eval_node() to produce a concrete value.

Use !ev for values that should be computed when the spec is instantiated, such as random samples, computed parameters, etc.

Example

YAML: count: !ev normal(50, 10) After hydrate: {"count": Evaluable(source="normal(50, 10)")} After eval: {"count": 47.3} # sampled value

Source code in src/alienbio/spec_lang/eval.py
@dataclass
class Evaluable:
    """Placeholder for !ev expressions - evaluated at instantiation time.

    Created during hydration when a !ev tag is encountered.
    Evaluated by eval_node() to produce a concrete value.

    Use !ev for values that should be computed when the spec is instantiated,
    such as random samples, computed parameters, etc.

    Example:
        YAML: count: !ev normal(50, 10)
        After hydrate: {"count": Evaluable(source="normal(50, 10)")}
        After eval: {"count": 47.3}  # sampled value
    """

    source: str

    def __repr__(self) -> str:
        return f"Evaluable({self.source!r})"

    def evaluate(self, namespace: dict[str, Any] | None = None) -> Any:
        """Evaluate the expression in a sandboxed namespace.

        Args:
            namespace: Dict of names available during evaluation

        Returns:
            Result of evaluating the expression
        """
        ns = namespace or {}
        blocked = {"open", "exec", "eval", "__import__", "compile", "globals", "locals"}
        builtins = __builtins__ if isinstance(__builtins__, dict) else vars(__builtins__)
        safe_builtins = {k: v for k, v in builtins.items() if k not in blocked}
        eval_ns = {"__builtins__": safe_builtins, **ns}
        return eval(self.source, eval_ns)

evaluate(namespace=None)

Evaluate the expression in a sandboxed namespace.

Parameters:

Name Type Description Default
namespace dict[str, Any] | None

Dict of names available during evaluation

None

Returns:

Type Description
Any

Result of evaluating the expression

Source code in src/alienbio/spec_lang/eval.py
def evaluate(self, namespace: dict[str, Any] | None = None) -> Any:
    """Evaluate the expression in a sandboxed namespace.

    Args:
        namespace: Dict of names available during evaluation

    Returns:
        Result of evaluating the expression
    """
    ns = namespace or {}
    blocked = {"open", "exec", "eval", "__import__", "compile", "globals", "locals"}
    builtins = __builtins__ if isinstance(__builtins__, dict) else vars(__builtins__)
    safe_builtins = {k: v for k, v in builtins.items() if k not in blocked}
    eval_ns = {"__builtins__": safe_builtins, **ns}
    return eval(self.source, eval_ns)

Quoted dataclass

Placeholder for !_ expressions - preserved as expression strings.

Created during hydration when a !_ or !quote tag is encountered. Preserved through evaluation - returns the source string unchanged. Used for rate equations, scoring functions, and other "code" that gets compiled or called later (not at instantiation time).

The !_ tag is the common case - most expressions in specs are lambdas.

Example

YAML: rate: !_ k * S After hydrate: {"rate": Quoted(source="k * S")} After eval: {"rate": "k * S"} # preserved for later compilation

Source code in src/alienbio/spec_lang/eval.py
@dataclass
class Quoted:
    """Placeholder for !_ expressions - preserved as expression strings.

    Created during hydration when a !_ or !quote tag is encountered.
    Preserved through evaluation - returns the source string unchanged.
    Used for rate equations, scoring functions, and other "code" that
    gets compiled or called later (not at instantiation time).

    The !_ tag is the common case - most expressions in specs are lambdas.

    Example:
        YAML: rate: !_ k * S
        After hydrate: {"rate": Quoted(source="k * S")}
        After eval: {"rate": "k * S"}  # preserved for later compilation
    """

    source: str

    def __repr__(self) -> str:
        return f"Quoted({self.source!r})"

Reference dataclass

Placeholder for !ref expressions.

Created during hydration when a !ref tag is encountered. Resolved during evaluation by looking up the name in ctx.bindings.

Example

YAML: permeability: !ref high_permeability After hydrate: {"permeability": Reference(name="high_permeability")} After eval: {"permeability": 0.8} # looked up from bindings

Source code in src/alienbio/spec_lang/eval.py
@dataclass
class Reference:
    """Placeholder for !ref expressions.

    Created during hydration when a !ref tag is encountered.
    Resolved during evaluation by looking up the name in ctx.bindings.

    Example:
        YAML: permeability: !ref high_permeability
        After hydrate: {"permeability": Reference(name="high_permeability")}
        After eval: {"permeability": 0.8}  # looked up from bindings
    """

    name: str

    def __repr__(self) -> str:
        return f"Reference({self.name!r})"

    def resolve(self, constants: dict[str, Any]) -> Any:
        """Resolve the reference from a constants dict.

        Supports dotted paths like "settings.threshold".

        Args:
            constants: Dict of named values to look up in

        Returns:
            The resolved value

        Raises:
            KeyError: If name not found in constants
        """
        parts = self.name.split(".")
        value: Any = constants
        for part in parts:
            if isinstance(value, dict) and part in value:
                value = value[part]
            else:
                raise KeyError(f"Cannot resolve reference: {self.name}")
        return value

resolve(constants)

Resolve the reference from a constants dict.

Supports dotted paths like "settings.threshold".

Parameters:

Name Type Description Default
constants dict[str, Any]

Dict of named values to look up in

required

Returns:

Type Description
Any

The resolved value

Raises:

Type Description
KeyError

If name not found in constants

Source code in src/alienbio/spec_lang/eval.py
def resolve(self, constants: dict[str, Any]) -> Any:
    """Resolve the reference from a constants dict.

    Supports dotted paths like "settings.threshold".

    Args:
        constants: Dict of named values to look up in

    Returns:
        The resolved value

    Raises:
        KeyError: If name not found in constants
    """
    parts = self.name.split(".")
    value: Any = constants
    for part in parts:
        if isinstance(value, dict) and part in value:
            value = value[part]
        else:
            raise KeyError(f"Cannot resolve reference: {self.name}")
    return value

EvalContext dataclass

Evaluation context for spec evaluation.

Carries state through the recursive evaluation process: - rng: Random number generator for reproducible sampling - bindings: Named values for !ref resolution - functions: Callable functions available to !_ expressions - path: Current location in the tree for error messages

Example

ctx = EvalContext( rng=np.random.default_rng(42), bindings={"k": 0.5, "permeability": 0.8}, functions={"normal": normal, "uniform": uniform} ) result = eval_node(hydrated_spec, ctx)

Source code in src/alienbio/spec_lang/eval.py
@dataclass
class EvalContext:
    """Evaluation context for spec evaluation.

    Carries state through the recursive evaluation process:
    - rng: Random number generator for reproducible sampling
    - bindings: Named values for !ref resolution
    - functions: Callable functions available to !_ expressions
    - path: Current location in the tree for error messages

    Example:
        ctx = EvalContext(
            rng=np.random.default_rng(42),
            bindings={"k": 0.5, "permeability": 0.8},
            functions={"normal": normal, "uniform": uniform}
        )
        result = eval_node(hydrated_spec, ctx)
    """

    rng: np.random.Generator = field(default_factory=np.random.default_rng)
    bindings: dict[str, Any] = field(default_factory=dict)
    functions: dict[str, Callable[..., Any]] = field(default_factory=dict)
    path: str = ""

    def child(self, key: str | int) -> "EvalContext":
        """Create child context with extended path."""
        new_path = f"{self.path}.{key}" if self.path else str(key)
        return EvalContext(
            rng=self.rng,
            bindings=self.bindings,
            functions=self.functions,
            path=new_path,
        )

child(key)

Create child context with extended path.

Source code in src/alienbio/spec_lang/eval.py
def child(self, key: str | int) -> "EvalContext":
    """Create child context with extended path."""
    new_path = f"{self.path}.{key}" if self.path else str(key)
    return EvalContext(
        rng=self.rng,
        bindings=self.bindings,
        functions=self.functions,
        path=new_path,
    )

EvalError

Bases: Exception

Error during spec evaluation.

Source code in src/alienbio/spec_lang/eval.py
class EvalError(Exception):
    """Error during spec evaluation."""

    def __init__(self, message: str, path: str = ""):
        self.path = path
        super().__init__(f"{path}: {message}" if path else message)

Scope

Bases: dict

A dict with lexical scoping (parent chain lookup).

Variables are inherited through the scope chain. Lookups check the current scope first, then climb to parent scopes until found.

The scope hierarchy is built at load time (via extends: in YAML), but variable lookups are dynamic - they climb the hierarchy at access time.

Attributes:

Name Type Description
parent

Optional parent Scope for inheritance chain

name

Optional name for this scope (for debugging)

Source code in src/alienbio/spec_lang/scope.py
class Scope(dict):
    """A dict with lexical scoping (parent chain lookup).

    Variables are inherited through the scope chain. Lookups check the
    current scope first, then climb to parent scopes until found.

    The scope hierarchy is built at load time (via `extends:` in YAML),
    but variable lookups are dynamic - they climb the hierarchy at
    access time.

    Attributes:
        parent: Optional parent Scope for inheritance chain
        name: Optional name for this scope (for debugging)
    """

    def __init__(
        self,
        data: dict[str, Any] | None = None,
        parent: Scope | None = None,
        name: str | None = None,
    ):
        """Create a new Scope.

        Args:
            data: Initial dict content
            parent: Parent scope for inheritance
            name: Optional name for debugging
        """
        super().__init__(data or {})
        self.parent = parent
        self.name = name

    def __getitem__(self, key: str) -> Any:
        """Get item, climbing parent chain if not found locally."""
        if key in self.keys():
            return super().__getitem__(key)
        if self.parent is not None:
            return self.parent[key]
        raise KeyError(key)

    def get(self, key: str, default: Any = None) -> Any:
        """Get item with default, climbing parent chain."""
        try:
            return self[key]
        except KeyError:
            return default

    def __contains__(self, key: object) -> bool:
        """Check if key exists in this scope or any parent."""
        if super().__contains__(key):
            return True
        if self.parent is not None:
            return key in self.parent
        return False

    def local_keys(self) -> Iterator[str]:
        """Return keys defined directly in this scope (not inherited)."""
        return iter(super().keys())

    def all_keys(self) -> set[str]:
        """Return all keys including inherited ones."""
        keys = set(super().keys())
        if self.parent is not None:
            keys |= self.parent.all_keys()
        return keys

    def child(self, data: dict[str, Any] | None = None, name: str | None = None) -> Scope:
        """Create a child scope that inherits from this one.

        Args:
            data: Initial content for child scope
            name: Optional name for the child scope

        Returns:
            New Scope with this scope as parent
        """
        return Scope(data, parent=self, name=name)

    def resolve(self, key: str) -> tuple[Any, Scope | None]:
        """Resolve a key and return (value, defining_scope).

        Useful for debugging to see where a value comes from.

        Args:
            key: The key to look up

        Returns:
            Tuple of (value, scope_that_defined_it)

        Raises:
            KeyError: If key not found in any scope
        """
        if key in self.keys():
            return super().__getitem__(key), self
        if self.parent is not None:
            return self.parent.resolve(key)
        raise KeyError(key)

    def __repr__(self) -> str:
        name_part = f" {self.name!r}" if self.name else ""
        parent_part = f" parent={self.parent.name!r}" if self.parent and self.parent.name else ""
        if not parent_part and self.parent:
            parent_part = " parent=<Scope>"
        return f"<Scope{name_part}{parent_part} {dict(self)}>"

__init__(data=None, parent=None, name=None)

Create a new Scope.

Parameters:

Name Type Description Default
data dict[str, Any] | None

Initial dict content

None
parent Scope | None

Parent scope for inheritance

None
name str | None

Optional name for debugging

None
Source code in src/alienbio/spec_lang/scope.py
def __init__(
    self,
    data: dict[str, Any] | None = None,
    parent: Scope | None = None,
    name: str | None = None,
):
    """Create a new Scope.

    Args:
        data: Initial dict content
        parent: Parent scope for inheritance
        name: Optional name for debugging
    """
    super().__init__(data or {})
    self.parent = parent
    self.name = name

__getitem__(key)

Get item, climbing parent chain if not found locally.

Source code in src/alienbio/spec_lang/scope.py
def __getitem__(self, key: str) -> Any:
    """Get item, climbing parent chain if not found locally."""
    if key in self.keys():
        return super().__getitem__(key)
    if self.parent is not None:
        return self.parent[key]
    raise KeyError(key)

get(key, default=None)

Get item with default, climbing parent chain.

Source code in src/alienbio/spec_lang/scope.py
def get(self, key: str, default: Any = None) -> Any:
    """Get item with default, climbing parent chain."""
    try:
        return self[key]
    except KeyError:
        return default

__contains__(key)

Check if key exists in this scope or any parent.

Source code in src/alienbio/spec_lang/scope.py
def __contains__(self, key: object) -> bool:
    """Check if key exists in this scope or any parent."""
    if super().__contains__(key):
        return True
    if self.parent is not None:
        return key in self.parent
    return False

local_keys()

Return keys defined directly in this scope (not inherited).

Source code in src/alienbio/spec_lang/scope.py
def local_keys(self) -> Iterator[str]:
    """Return keys defined directly in this scope (not inherited)."""
    return iter(super().keys())

all_keys()

Return all keys including inherited ones.

Source code in src/alienbio/spec_lang/scope.py
def all_keys(self) -> set[str]:
    """Return all keys including inherited ones."""
    keys = set(super().keys())
    if self.parent is not None:
        keys |= self.parent.all_keys()
    return keys

child(data=None, name=None)

Create a child scope that inherits from this one.

Parameters:

Name Type Description Default
data dict[str, Any] | None

Initial content for child scope

None
name str | None

Optional name for the child scope

None

Returns:

Type Description
Scope

New Scope with this scope as parent

Source code in src/alienbio/spec_lang/scope.py
def child(self, data: dict[str, Any] | None = None, name: str | None = None) -> Scope:
    """Create a child scope that inherits from this one.

    Args:
        data: Initial content for child scope
        name: Optional name for the child scope

    Returns:
        New Scope with this scope as parent
    """
    return Scope(data, parent=self, name=name)

resolve(key)

Resolve a key and return (value, defining_scope).

Useful for debugging to see where a value comes from.

Parameters:

Name Type Description Default
key str

The key to look up

required

Returns:

Type Description
tuple[Any, Scope | None]

Tuple of (value, scope_that_defined_it)

Raises:

Type Description
KeyError

If key not found in any scope

Source code in src/alienbio/spec_lang/scope.py
def resolve(self, key: str) -> tuple[Any, Scope | None]:
    """Resolve a key and return (value, defining_scope).

    Useful for debugging to see where a value comes from.

    Args:
        key: The key to look up

    Returns:
        Tuple of (value, scope_that_defined_it)

    Raises:
        KeyError: If key not found in any scope
    """
    if key in self.keys():
        return super().__getitem__(key), self
    if self.parent is not None:
        return self.parent.resolve(key)
    raise KeyError(key)

compile_rate_expression(source, constants)

Compile a rate expression string into a callable function.

Constants are baked in at compile time (copied, not referenced). Variables (S, S1, S2, P, P1, P2, or molecule names) are looked up from the state dict passed at runtime.

Parameters:

Name Type Description Default
source str

Rate expression string, e.g. "k * S"

required
constants dict[str, float]

Named constants to bake in, e.g. {"k": 0.1}

required

Returns:

Type Description
Callable[[dict[str, float]], float]

A function (state: dict[str, float]) -> float

Source code in src/alienbio/spec_lang/rate_compiler.py
def compile_rate_expression(
    source: str,
    constants: dict[str, float],
) -> Callable[[dict[str, float]], float]:
    """Compile a rate expression string into a callable function.

    Constants are baked in at compile time (copied, not referenced).
    Variables (S, S1, S2, P, P1, P2, or molecule names) are looked up
    from the state dict passed at runtime.

    Args:
        source: Rate expression string, e.g. ``"k * S"``
        constants: Named constants to bake in, e.g. ``{"k": 0.1}``

    Returns:
        A function ``(state: dict[str, float]) -> float``
    """
    code = compile(source, "<rate>", "eval")
    baked = {**_RATE_MATH, **dict(constants)}  # copy constants

    def rate_fn(state: dict[str, float]) -> float:
        namespace = {**baked, **state}
        return float(eval(code, _RATE_BUILTINS, namespace))

    return rate_fn

compile_sim(scenario, dt=1.0)

Create a CompiledSimulator from a scenario spec.

Parameters:

Name Type Description Default
scenario ScenarioSpec | dict[str, Any]

ScenarioSpec or dict with keys name, molecules, reactions, initial_state, scope

required
dt float

Timestep size (default 1.0)

1.0

Returns:

Type Description
CompiledSimulator

CompiledSimulator ready to run

Source code in src/alienbio/spec_lang/compiled_sim.py
def compile_sim(scenario: ScenarioSpec | dict[str, Any], dt: float = 1.0) -> CompiledSimulator:
    """Create a CompiledSimulator from a scenario spec.

    Args:
        scenario: ScenarioSpec or dict with keys name, molecules, reactions,
                  initial_state, scope
        dt: Timestep size (default 1.0)

    Returns:
        CompiledSimulator ready to run
    """
    if isinstance(scenario, dict):
        scenario = ScenarioSpec(
            name=scenario.get("name", "unnamed"),
            molecules=scenario.get("molecules", {}),
            reactions=scenario.get("reactions", {}),
            initial_state=scenario.get("initial_state", {}),
            scope=scenario.get("scope", {}),
        )
    return CompiledSimulator(scenario, dt=dt)

biotype(arg=None)

biotype(cls: type[T]) -> type[T]
biotype(name: str) -> Callable[[type[T]], type[T]]

Register a class for hydration from YAML.

Usage

@biotype class Chemistry: ...

@biotype("custom_name") class World: ...

Source code in src/alienbio/spec_lang/decorators.py
def biotype(arg: type[T] | str | None = None) -> type[T] | Callable[[type[T]], type[T]]:
    """Register a class for hydration from YAML.

    Usage:
        @biotype
        class Chemistry: ...

        @biotype("custom_name")
        class World: ...
    """

    def decorator(cls: type[T]) -> type[T]:
        type_name = arg if isinstance(arg, str) else cls.__name__.lower()
        biotype_registry[type_name] = cls
        # Add _biotype_name attribute for dehydration
        cls._biotype_name = type_name  # type: ignore
        return cls

    if isinstance(arg, type):
        # Called as @biotype without parens
        return decorator(arg)
    else:
        # Called as @biotype("name") or @biotype()
        return decorator

fn(summary=None, range=None, **kwargs)

Base decorator for all functions. Stores metadata.

Parameters:

Name Type Description Default
summary str | None

Short description for plots/tables

None
range tuple[float, float] | None

Expected output range

None
**kwargs Any

Additional metadata

{}
Source code in src/alienbio/spec_lang/decorators.py
def fn(
    summary: str | None = None,
    range: tuple[float, float] | None = None,
    **kwargs: Any,
) -> Callable[[F], F]:
    """Base decorator for all functions. Stores metadata.

    Args:
        summary: Short description for plots/tables
        range: Expected output range
        **kwargs: Additional metadata
    """

    def decorator(func: F) -> F:
        wrapped = FnMeta(func, summary=summary, range=range, **kwargs)
        wraps(func)(wrapped)
        return wrapped  # type: ignore

    return decorator

scoring(summary=None, range=(0.0, 1.0), higher_is_better=True, **kwargs)

Decorator for scoring functions.

Registers function in scoring_registry.

Source code in src/alienbio/spec_lang/decorators.py
def scoring(
    summary: str | None = None,
    range: tuple[float, float] = (0.0, 1.0),
    higher_is_better: bool = True,
    **kwargs: Any,
) -> Callable[[F], F]:
    """Decorator for scoring functions.

    Registers function in scoring_registry.
    """

    def decorator(func: F) -> F:
        wrapped = FnMeta(
            func,
            summary=summary,
            range=range,
            higher_is_better=higher_is_better,
            **kwargs,
        )
        wraps(func)(wrapped)
        scoring_registry[func.__name__] = wrapped
        return wrapped  # type: ignore

    return decorator

action(summary=None, targets=None, reversible=False, cost=1.0, **kwargs)

Decorator for action functions.

Registers function in action_registry.

Source code in src/alienbio/spec_lang/decorators.py
def action(
    summary: str | None = None,
    targets: str | None = None,
    reversible: bool = False,
    cost: float = 1.0,
    **kwargs: Any,
) -> Callable[[F], F]:
    """Decorator for action functions.

    Registers function in action_registry.
    """

    def decorator(func: F) -> F:
        wrapped = FnMeta(
            func,
            summary=summary,
            targets=targets,
            reversible=reversible,
            cost=cost,
            **kwargs,
        )
        wraps(func)(wrapped)
        action_registry[func.__name__] = wrapped
        return wrapped  # type: ignore

    return decorator

measurement(summary=None, targets=None, cost='none', **kwargs)

Decorator for measurement functions.

Registers function in measurement_registry.

Source code in src/alienbio/spec_lang/decorators.py
def measurement(
    summary: str | None = None,
    targets: str | None = None,
    cost: str = "none",
    **kwargs: Any,
) -> Callable[[F], F]:
    """Decorator for measurement functions.

    Registers function in measurement_registry.
    """

    def decorator(func: F) -> F:
        wrapped = FnMeta(
            func,
            summary=summary,
            targets=targets,
            cost=cost,
            **kwargs,
        )
        wraps(func)(wrapped)
        measurement_registry[func.__name__] = wrapped
        return wrapped  # type: ignore

    return decorator

rate(summary=None, range=(0.0, float('inf')), **kwargs)

Decorator for rate functions.

Registers function in rate_registry.

Source code in src/alienbio/spec_lang/decorators.py
def rate(
    summary: str | None = None,
    range: tuple[float, float] = (0.0, float("inf")),
    **kwargs: Any,
) -> Callable[[F], F]:
    """Decorator for rate functions.

    Registers function in rate_registry.
    """

    def decorator(func: F) -> F:
        wrapped = FnMeta(
            func,
            summary=summary,
            range=range,
            **kwargs,
        )
        wraps(func)(wrapped)
        rate_registry[func.__name__] = wrapped
        return wrapped  # type: ignore

    return decorator

get_biotype(name)

Get a biotype class by name.

Raises:

Type Description
KeyError

If name not registered

Source code in src/alienbio/spec_lang/decorators.py
def get_biotype(name: str) -> type:
    """Get a biotype class by name.

    Raises:
        KeyError: If name not registered
    """
    if name not in biotype_registry:
        raise KeyError(f"Unknown biotype: {name}")
    return biotype_registry[name]

get_action(name)

Get an action by name.

Raises:

Type Description
KeyError

If name not registered

Source code in src/alienbio/spec_lang/decorators.py
def get_action(name: str) -> Callable:
    """Get an action by name.

    Raises:
        KeyError: If name not registered
    """
    if name not in action_registry:
        raise KeyError(f"Unknown action: {name}")
    return action_registry[name]

get_measurement(name)

Get a measurement by name.

Raises:

Type Description
KeyError

If name not registered

Source code in src/alienbio/spec_lang/decorators.py
def get_measurement(name: str) -> Callable:
    """Get a measurement by name.

    Raises:
        KeyError: If name not registered
    """
    if name not in measurement_registry:
        raise KeyError(f"Unknown measurement: {name}")
    return measurement_registry[name]

get_scoring(name)

Get a scoring function by name.

Raises:

Type Description
KeyError

If name not registered

Source code in src/alienbio/spec_lang/decorators.py
def get_scoring(name: str) -> Callable:
    """Get a scoring function by name.

    Raises:
        KeyError: If name not registered
    """
    if name not in scoring_registry:
        raise KeyError(f"Unknown scoring function: {name}")
    return scoring_registry[name]

get_rate(name)

Get a rate function by name.

Raises:

Type Description
KeyError

If name not registered

Source code in src/alienbio/spec_lang/decorators.py
def get_rate(name: str) -> Callable:
    """Get a rate function by name.

    Raises:
        KeyError: If name not registered
    """
    if name not in rate_registry:
        raise KeyError(f"Unknown rate function: {name}")
    return rate_registry[name]

hydrate(data, base_path=None)

Convert dict structure to Python objects with placeholders.

Transforms

{"!_": source} → Quoted(source) (preserve expression) {"!ev": source} → Evaluable(source) (evaluate at instantiation) {"!ref": name} → Reference(name) (lookup in bindings) {"!include": path} → file contents (recursively hydrated)

Also handles
  • Recursive descent into dicts and lists
  • YAML tag objects (Evaluable, Quoted, Reference, Include) pass through
  • Include objects are loaded and their contents hydrated

Parameters:

Name Type Description Default
data Any

The data structure to hydrate

required
base_path str | None

Base directory for resolving !include paths

None

Returns:

Type Description
Any

Hydrated data with placeholders

Raises:

Type Description
FileNotFoundError

If !include file doesn't exist

Source code in src/alienbio/spec_lang/eval.py
def hydrate(data: Any, base_path: str | None = None) -> Any:
    """Convert dict structure to Python objects with placeholders.

    Transforms:
        {"!_": source} → Quoted(source)       (preserve expression)
        {"!ev": source} → Evaluable(source)   (evaluate at instantiation)
        {"!ref": name} → Reference(name)      (lookup in bindings)
        {"!include": path} → file contents    (recursively hydrated)

    Also handles:
        - Recursive descent into dicts and lists
        - YAML tag objects (Evaluable, Quoted, Reference, Include) pass through
        - Include objects are loaded and their contents hydrated

    Args:
        data: The data structure to hydrate
        base_path: Base directory for resolving !include paths

    Returns:
        Hydrated data with placeholders

    Raises:
        FileNotFoundError: If !include file doesn't exist
    """
    return _hydrate_node(data, base_path)

dehydrate(data)

Convert Python objects back to serializable dict structure.

Inverse of hydrate() - converts placeholder objects back to their dict representation for YAML serialization.

Transforms

Evaluable(source) → {"!ev": source} (evaluate at instantiation) Quoted(source) → {"!_": source} (preserve expression) Reference(name) → {"!ref": name}

Parameters:

Name Type Description Default
data Any

The data structure to dehydrate

required

Returns:

Type Description
Any

Dehydrated data suitable for YAML serialization

Source code in src/alienbio/spec_lang/eval.py
def dehydrate(data: Any) -> Any:
    """Convert Python objects back to serializable dict structure.

    Inverse of hydrate() - converts placeholder objects back to their
    dict representation for YAML serialization.

    Transforms:
        Evaluable(source) → {"!ev": source}  (evaluate at instantiation)
        Quoted(source) → {"!_": source}      (preserve expression)
        Reference(name) → {"!ref": name}

    Args:
        data: The data structure to dehydrate

    Returns:
        Dehydrated data suitable for YAML serialization
    """
    return _dehydrate_node(data)

eval_node(node, ctx, *, strict=True)

Recursively evaluate a hydrated spec node.

Processes placeholder objects

Evaluable → execute expression and return result Quoted → return source string unchanged Reference → look up in ctx.bindings

Recursively evaluates

dict → evaluate all values list → evaluate all elements

Passes through

Scalar values (int, float, str, bool, None)

Parameters:

Name Type Description Default
node Any

The hydrated node to evaluate

required
ctx EvalContext

Evaluation context

required
strict bool

If True, missing references raise EvalError. If False, missing references return the Reference unchanged.

True

Returns:

Type Description
Any

Fully evaluated value

Raises:

Type Description
EvalError

If evaluation fails (undefined reference, syntax error, etc.)

Source code in src/alienbio/spec_lang/eval.py
def eval_node(node: Any, ctx: EvalContext, *, strict: bool = True) -> Any:
    """Recursively evaluate a hydrated spec node.

    Processes placeholder objects:
        Evaluable → execute expression and return result
        Quoted → return source string unchanged
        Reference → look up in ctx.bindings

    Recursively evaluates:
        dict → evaluate all values
        list → evaluate all elements

    Passes through:
        Scalar values (int, float, str, bool, None)

    Args:
        node: The hydrated node to evaluate
        ctx: Evaluation context
        strict: If True, missing references raise EvalError.
                If False, missing references return the Reference unchanged.

    Returns:
        Fully evaluated value

    Raises:
        EvalError: If evaluation fails (undefined reference, syntax error, etc.)
    """
    # Evaluable - execute the expression
    if isinstance(node, Evaluable):
        return _eval_expression(node.source, ctx)

    # Quoted - return the source string unchanged
    if isinstance(node, Quoted):
        return node.source

    # Reference - look up in bindings
    if isinstance(node, Reference):
        if node.name not in ctx.bindings:
            if not strict:
                return node
            raise EvalError(f"Undefined reference: {node.name!r}", ctx.path)
        return ctx.bindings[node.name]

    # Dict - recurse into values
    if isinstance(node, dict):
        return {k: eval_node(v, ctx.child(k), strict=strict) for k, v in node.items()}

    # List - recurse into elements
    if isinstance(node, list):
        return [eval_node(item, ctx.child(i), strict=strict) for i, item in enumerate(node)]

    # Scalar values - pass through
    return node

make_context(seed=None, bindings=None, functions=None)

Create an evaluation context with default functions.

Convenience function that sets up a Context with: - Seeded RNG for reproducibility - Optional custom bindings - DEFAULT_FUNCTIONS plus any custom functions

Parameters:

Name Type Description Default
seed int | None

Random seed for reproducibility (None for random)

None
bindings dict[str, Any] | None

Named values for !ref resolution

None
functions dict[str, Callable[..., Any]] | None

Additional functions (merged with defaults)

None

Returns:

Type Description
EvalContext

Configured EvalContext ready for evaluation

Source code in src/alienbio/spec_lang/eval.py
def make_context(
    seed: int | None = None,
    bindings: dict[str, Any] | None = None,
    functions: dict[str, Callable[..., Any]] | None = None,
) -> EvalContext:
    """Create an evaluation context with default functions.

    Convenience function that sets up a Context with:
    - Seeded RNG for reproducibility
    - Optional custom bindings
    - DEFAULT_FUNCTIONS plus any custom functions

    Args:
        seed: Random seed for reproducibility (None for random)
        bindings: Named values for !ref resolution
        functions: Additional functions (merged with defaults)

    Returns:
        Configured EvalContext ready for evaluation
    """
    rng = np.random.default_rng(seed)

    all_functions = dict(DEFAULT_FUNCTIONS)
    if functions:
        all_functions.update(functions)

    return EvalContext(
        rng=rng,
        bindings=bindings or {},
        functions=all_functions,
    )

normal(mean, std, *, ctx)

Sample from normal distribution.

Source code in src/alienbio/spec_lang/builtins.py
def normal(mean: float, std: float, *, ctx: "Context") -> float:
    """Sample from normal distribution."""
    return float(ctx.rng.normal(mean, std))

uniform(low, high, *, ctx)

Sample from uniform distribution.

Source code in src/alienbio/spec_lang/builtins.py
def uniform(low: float, high: float, *, ctx: "Context") -> float:
    """Sample from uniform distribution."""
    return float(ctx.rng.uniform(low, high))

lognormal(mean, sigma, *, ctx)

Sample from log-normal distribution.

Source code in src/alienbio/spec_lang/builtins.py
def lognormal(mean: float, sigma: float, *, ctx: "Context") -> float:
    """Sample from log-normal distribution."""
    return float(ctx.rng.lognormal(mean, sigma))

poisson(lam, *, ctx)

Sample from Poisson distribution.

Source code in src/alienbio/spec_lang/builtins.py
def poisson(lam: float, *, ctx: "Context") -> int:
    """Sample from Poisson distribution."""
    return int(ctx.rng.poisson(lam))

exponential(scale, *, ctx)

Sample from exponential distribution.

Source code in src/alienbio/spec_lang/builtins.py
def exponential(scale: float, *, ctx: "Context") -> float:
    """Sample from exponential distribution."""
    return float(ctx.rng.exponential(scale))

choice(options, *, ctx)

Choose uniformly from a list.

Source code in src/alienbio/spec_lang/builtins.py
def choice(options: list[Any], *, ctx: "Context") -> Any:
    """Choose uniformly from a list."""
    idx = ctx.rng.integers(0, len(options))
    return options[idx]

discrete(weights, *, ctx)

Sample index from discrete distribution with given weights.

Source code in src/alienbio/spec_lang/builtins.py
def discrete(weights: list[float], *, ctx: "Context") -> int:
    """Sample index from discrete distribution with given weights."""
    probs = np.array(weights, dtype=float)
    probs = probs / probs.sum()
    return int(ctx.rng.choice(len(weights), p=probs))

transform_typed_keys(data, type_registry=None)

Transform type.name keys to nested structure with _type field.

Parameters:

Name Type Description Default
data dict[str, Any]

Dict with keys like "world.foo", "suite.bar"

required
type_registry set[str] | None

Set of known type names (default: built-in types)

None

Returns:

Type Description
dict[str, Any]

Transformed dict with _type fields

Example

{"world.foo": {"molecules": {}}} becomes: {"foo": {"_type": "world", "molecules": {}}}

Source code in src/alienbio/spec_lang/loader.py
def transform_typed_keys(data: dict[str, Any], type_registry: set[str] | None = None) -> dict[str, Any]:
    """Transform type.name keys to nested structure with _type field.

    Args:
        data: Dict with keys like "world.foo", "suite.bar"
        type_registry: Set of known type names (default: built-in types)

    Returns:
        Transformed dict with _type fields

    Example:
        {"world.foo": {"molecules": {}}}
        becomes:
        {"foo": {"_type": "world", "molecules": {}}}
    """
    if type_registry is None:
        type_registry = {"suite", "scenario"}

    result: dict[str, Any] = {}

    for key, value in data.items():
        if "." in key and isinstance(value, dict):
            type_name, rest = key.split(".", 1)

            if type_name in type_registry:
                # Recursively transform nested typed keys in value
                transformed_value = transform_typed_keys(value, type_registry)

                # Add _type field
                transformed_value = {"_type": type_name, **transformed_value}

                # Store under the rest of the name
                result[rest] = transformed_value
            else:
                # Not a known type, keep as-is but still recurse
                result[key] = transform_typed_keys(value, type_registry)
        elif isinstance(value, dict):
            # Recurse into non-typed dicts
            result[key] = transform_typed_keys(value, type_registry)
        else:
            result[key] = value

    return result

expand_defaults(data, inherited_defaults=None)

Expand defaults through suite/scenario hierarchy.

Parameters:

Name Type Description Default
data dict[str, Any]

Dict with suite/scenario structure and defaults

required
inherited_defaults dict[str, Any] | None

Defaults inherited from parent suites

None

Returns:

Type Description
dict[str, Any]

Data with defaults expanded into each scenario

Source code in src/alienbio/spec_lang/loader.py
def expand_defaults(data: dict[str, Any], inherited_defaults: dict[str, Any] | None = None) -> dict[str, Any]:
    """Expand defaults through suite/scenario hierarchy.

    Args:
        data: Dict with suite/scenario structure and defaults
        inherited_defaults: Defaults inherited from parent suites

    Returns:
        Data with defaults expanded into each scenario
    """
    result = copy.deepcopy(data)
    inherited = inherited_defaults or {}

    def process_node(node: dict[str, Any], parent_defaults: dict[str, Any]) -> dict[str, Any]:
        """Process a single node, applying defaults to scenarios."""
        if not isinstance(node, dict):
            return node

        node_type = node.get("_type")

        if node_type == "suite":
            # Get this suite's defaults, merged with inherited
            suite_defaults = node.get("defaults", {})
            combined_defaults = deep_merge(parent_defaults, suite_defaults)

            # Process all children
            new_node = {}
            for key, value in node.items():
                if key in ("_type", "defaults"):
                    new_node[key] = value
                elif isinstance(value, dict):
                    new_node[key] = process_node(value, combined_defaults)
                else:
                    new_node[key] = value
            return new_node

        elif node_type == "scenario":
            # Apply defaults to scenario (defaults first, then scenario values)
            scenario_values = {k: v for k, v in node.items() if k != "_type"}
            merged = deep_merge(parent_defaults, scenario_values)
            merged["_type"] = "scenario"
            return merged

        else:
            # Not a suite or scenario - recurse into children
            new_node = {}
            for key, value in node.items():
                if isinstance(value, dict):
                    new_node[key] = process_node(value, parent_defaults)
                else:
                    new_node[key] = value
            return new_node

    # Process top-level items
    for key, value in result.items():
        if isinstance(value, dict):
            result[key] = process_node(value, inherited)

    return result