<!-- Source: https://docs.biosimulant.com/how-to/create-biomodule -->

# How to Create a BioModule

This guide builds a temporal module against the communication-step kernel and
shows how finite computations use the same `BioModule` contract.

## Project structure

```text
my-model/
  src/
    my_module.py
  model.yaml
  requirements.txt
```

## Step 1: implement the module

```python
# src/my_module.py

class PopulationCounter(biosim.BioModule):
    execution_policy = biosim.ExecutionPolicy.EACH_WINDOW

    def __init__(self, initial_count: int = 100, growth_rate: float = 0.1):
        self.initial_count = int(initial_count)
        self.count = int(initial_count)
        self.growth_rate = float(growth_rate)
        self.history = []

    def inputs(self):
        return {
            "deaths": biosim.SignalSpec.scalar(
                dtype="int64",
                max_age=1.0,
                stale_policy="warn",
            )
        }

    def outputs(self):
        return {
            "population_state": biosim.SignalSpec.record(
                schema={"count": "int64"},
                description="Current population count",
            )
        }

    def execute(self, inputs, *, context):
        deaths = inputs.get("deaths")
        if deaths is not None:
            self.count -= int(deaths.value)
        growth = int(self.count * self.growth_rate)
        self.count += growth
        end = context.window_end
        assert end is not None
        self.history.append([end, self.count])
        return {"population_state": {"count": self.count}}

    def snapshot(self):
        return {
            "count": self.count,
            "history": list(self.history),
        }

    def restore(self, snapshot):
        self.count = int(snapshot["count"])
        self.history = [list(point) for point in snapshot.get("history", [])]

    def visualize(self):
        return {
            "render": "timeseries",
            "data": {
                "title": "Population Over Time",
                "xlabel": "Time",
                "ylabel": "Count",
                "series": [{"name": "Population", "points": self.history}],
            },
        }
```

## Step 2: smoke-test it locally

```python

from src.my_module import PopulationCounter

world = biosim.BioWorld(communication_step=1.0)
builder = biosim.WiringBuilder(world)
builder.add("population", PopulationCounter(initial_count=50, growth_rate=0.05))
builder.apply()

world.run(duration=10.0)
print(world.collect_visuals())
```

## Choose a policy

The example above uses `EACH_WINDOW` because its state changes every window. Use
`ONCE_BEFORE_RUN` for one-shot preprocessing or inference, and `ONCE_AFTER_RUN`
for final analysis or export. Existing fixed-step models that need zero-time
settling can keep `advance_window()` or `StatefulBioModule`. The
[BioModule API](/references/biomodule-api) covers both. No manifest
execution field is required.

## Step 3: write the manifest

```yaml
schema_version: "2.0"
title: "Population Counter"
description: "Simple growth model with an optional deaths input"
standard: other

biosim:
  entrypoint: "src.my_module:PopulationCounter"
  init_kwargs:
    initial_count: 100
    growth_rate: 0.1
  communication_step: 1.0

io:
  inputs:
    - name: deaths
      signal_type: scalar
      dtype: int64
  outputs:
    - name: population_state
      signal_type: record
      schema:
        count: int64

runtime:
  python_version: "3.12"
  dependencies:
    packages: []
```

## Step 4: build the package

Add this model to `biosimulant-packages.yaml`, then build from the repository root:

```bash
biosimulant labs release build biosimulant-packages.yaml --out dist/biosimulant-packages
```

## Notes

- Temporal hooks emit typed signals. Execute-style hooks may return raw values;
  BioWorld validates and wraps them using declared output specs, then replaces any
  caller-supplied timestamp with `context.simulated_time`.
- Direct `execute()` calls return the author's raw mapping and do not update
  `get_outputs()`; only a successful BioWorld commit updates that cache.
- Canonical modules do not run during zero-time `settle()`.
- Declare ports as `dict[str, SignalSpec]`, not sets of strings.
- Use `snapshot()` / `restore()` for replay-safe state. Do not rely on a kernel-level reset API.
- Use `reset()` only if you own a higher-level workflow that calls it explicitly.

**Warning:**

  A consumer may not receive a signal on every boundary. Always treat `inputs.get("port")` as optional.

## Next steps

- [Use a BioWorld](/how-to/use-bioworld)
- [Test a BioModule](/how-to/test-biomodule)
- [Artifact Outputs](/references/artifact-outputs)
- [BioModule API Reference](/references/biomodule-api)
