Metadata-Version: 2.4
Name: coconet-python
Version: 0.4.4
Summary: Headless Python port of the legacy CoCoNet NetLogo model.
Requires-Python: >=3.11
Requires-Dist: numpy>=2.2.0
Requires-Dist: pandas>=2.2.0
Requires-Dist: pyyaml>=6.0.0
Requires-Dist: threadpoolctl>=3.5.0
Provides-Extra: profile
Requires-Dist: pyinstrument>=5.0.0; extra == 'profile'
Description-Content-Type: text/markdown

# CoCoNet Python Port

This repository contains a headless Python port of the legacy NetLogo CoCoNet model.

**User documentation (Jekyll):** the [`documentation/`](documentation/) directory holds a static site with installation, configuration, API, and I/O reference. On GitHub, enable **Settings → Pages → GitHub Actions**; pushes to `main` then publish to `https://<owner>.github.io/<repo>/` via the “Deploy documentation to GitHub Pages” workflow.

## Two ways to run CoCoNet

The same simulation engine is exposed for **shell-oriented workflows** and for **programmatic control**. Pick the entry point that matches how you are integrating the model.

### 1. Command-line interface (CLI)

Use this when you want **files, flags, and environment variables**—for example local terminals, Docker (the published image uses the CLI as its entrypoint), shell scripts, or CI jobs that only need to pass paths.

- **Console script:** `coconet` (installed with the package).
- **Module form:** `python -m coconet` (same behaviour as `coconet`).
- **Typical inputs:** `--config` (YAML), `--parameter-file` (legacy CSV), `--reefs-file`, `--coastline-file`, `--output-file`, plus `COCONET_*` environment overrides. See **`coconet --help`** and the [CLI documentation](documentation/cli.md).

The CLI handles **logging bootstrap**, optional **CPU profiling** (`--profile`, extra dependency), and applies a fixed set of **command-line overrides** on top of the shared configuration loader.

### 2. Python library (PyPI package **coconet-python**)

Use this when you need **full control from Python**—custom CLIs, web services, notebooks, schedulers, or multi-step pipelines. Install from **PyPI** (distribution name **`coconet-python`**; import package remains **`coconet`**):

```bash
pip install coconet-python
```

The stable library surface is **`load_coconet_config`** and **`run_coconet`** in `coconet.api` (re-exported from `coconet`). Build a `CoconetConfig` from YAML paths, legacy CSV, environment, an optional **`scenario`** dict, and keyword overrides, then run:

```python
from coconet import load_coconet_config, run_coconet

cfg = load_coconet_config(
    config_file="config/example.yaml",
    reefs_file="legacy/reefs2024.csv",
    coastline_file="legacy/coastline.csv",
    output_file="out/run.csv",
)
run_coconet(cfg, configure_logs=True)
```

For **advanced** embedding you can still construct **`CoconetModel`** directly from **`CoconetConfig`**. See the [Python API](documentation/python-api.md) page for precedence, logging, and `__all__`.

## Why this exists

The `legacy/` directory contains the original monolithic NetLogo model (`CoCoNet V3_rubble.nlogo`) and its data files. This port migrates the model to a modern Python stack for:

- headless execution in cloud/CI environments,
- typed, maintainable code,
- easier configuration through files and environment variables,
- reproducible scenario runs without a GUI.

## Project layout

- `legacy/` - original NetLogo model and data files.
- `coconet/` - Python implementation:
  - `config.py` - typed configuration and legacy parameter CSV parsing,
  - `model.py` - simulation engine (ported procedures),
  - `api.py` - **library** entry points (`load_coconet_config`, `run_coconet`) for PyPI consumers and embedders,
  - `cli.py` - **CLI** entry point (`coconet` / `python -m coconet`),
  - `__main__.py` - delegates to the CLI for `python -m coconet`.

## Running with uv (from a git checkout)

```bash
uv sync
uv run coconet --parameter-file legacy/I_2p6.csv --output-file output.csv
```

You can also use YAML config:

```bash
uv run coconet --config config/example.yaml --output-file output.csv
```

## Docker (GitHub Container Registry)

A container image is built with [GitHub Actions](.github/workflows/docker-publish.yml) on pushes to the default branch (`main`) and on SemVer tags `v*`. It is published to **GitHub Container Registry** as [`ghcr.io/gbrrestoration/coconet-python`](https://github.com/gbrrestoration/coconet-python/pkgs/container/coconet-python) (pull: `docker pull ghcr.io/gbrrestoration/coconet-python:latest`).

The image is based on `python:3.12-slim-bookworm`, installs dependencies with **uv** (`uv sync --frozen`), bundles `legacy/` and `config/` under `/app`, and runs as UID **1000**. The container entrypoint is the **`coconet` CLI** (see [Two ways to run CoCoNet](#two-ways-to-run-coconet)). If you need the **library** interface instead, add `pip install coconet-python` (or copy the package) in your own image and call `load_coconet_config` / `run_coconet` from your code.

### Input and output paths

Point the CLI at inputs and outputs using flags (or YAML / environment variables). For data on the host, mount a host directory into the container and pass **container paths** to the CLI.

| Item | CLI flags | Environment (optional) |
| --- | --- | --- |
| Reef table CSV | `--reefs-file` | `COCONET_REEFS_FILE` |
| Coastline CSV | `--coastline-file` | `COCONET_COASTLINE_FILE` |
| Scenario YAML | `--config` | (YAML keys `reefs_file`, `coastline_file`, …) |
| Legacy parameter CSV | `--parameter-file` | (set via YAML `parameter_file` if needed) |
| Run output CSV | `--output-file` | `COCONET_OUTPUT_FILE` |

**Example: bundled inputs, outputs on the host**

```bash
mkdir -p ./out
docker run --rm \
  -v "$(pwd)/out:/out" \
  ghcr.io/gbrrestoration/coconet-python:latest \
  --config /app/config/example.yaml \
  --output-file /out/run.csv
```

**Example: custom inputs and outputs on the host**

```bash
mkdir -p ./out
docker run --rm \
  -v "/path/to/mydata:/data:ro" \
  -v "$(pwd)/out:/out" \
  ghcr.io/gbrrestoration/coconet-python:latest \
  --reefs-file /data/reefs2024.csv \
  --coastline-file /data/coastline.csv \
  --parameter-file /data/myparams.csv \
  --output-file /out/results.csv
```

Use `-e COCONET_ENSEMBLE_RUNS=2` (and other `COCONET_*` variables) if you prefer environment overrides; see below. Run `docker run --rm ghcr.io/gbrrestoration/coconet-python:latest --help` for all CLI options.

**Permissions:** the process in the image runs as UID **1000** (`coconet`). Writable bind mounts (for `--output-file`, profiling output, etc.) must allow that user to create files—for example `sudo chown 1000:1000 ./out` on the host directory, or a Docker volume instead of a root-owned host path. Overriding with `--user` is not recommended because `/app` in the image is not world-readable.

Environment overrides are supported with `COCONET_` prefix, for example:

```bash
export COCONET_ENSEMBLE_RUNS=2
export COCONET_END_YEAR=1990
uv run coconet --parameter-file legacy/I_2p6.csv
```

## Logging

The CLI includes lifecycle logging for setup, progress, and run completion.

You can control logging level with either CLI or environment variables:

```bash
uv run coconet --config config/example.yaml --log-level DEBUG
```

```bash
export COCONET_LOG_LEVEL=WARNING
uv run coconet --config config/example.yaml
```

Supported levels are: `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`.

If both are provided, CLI takes precedence over the environment variable.

## Notes on parity

The implementation ports the NetLogo procedures and keeps important NetLogo semantics where feasible (seed resets, annual loop structure, reef/site-level state updates, interventions, and output schema). This is designed to support model-output comparison workflows between the legacy and Python implementations.

## Legacy parameter CSV (`--parameter-file`)

The documentation site (Jekyll under `documentation/`, page **Scenario parameter semantics**) carries the same tables as the following sections for easier browsing once GitHub Pages is enabled.

Rows are `Label, value` pairs. Blank lines are skipped. Parsing is **case-insensitive** on the label. The first row whose first cell is exactly `Ensemble` (the output table header) ends the config block; everything after that is treated as data rows, not parameters.

Labels are mapped in `coconet/config.py` (`label_to_attr`). If a label from a template file is **missing**, the value stays at the Python default in `CoconetConfig` (for example `legacy/I_2p6.csv` has no `Spinup backtrack (years)` row, so the default `50` is used, and no `CoTS vessels in active sector` row, so default `0` is used).

**YAML / environment-only** (not read from the legacy CSV): `reefs_file`, `coastline_file`, `output_file`, `ensemble_threads`, `log_level`, `search_mode`, `perfect_intervention`, `unregulated_fishing`. Override these via `--config`, `COCONET_*` env vars, or CLI flags (`--reefs-file`, `--coastline-file`, `--output-file`, …) where supported.

### Simulation schedule

| Legacy CSV label | `CoconetConfig` field | Role |
| --- | --- | --- |
| Climate scenario | `SSP` | Shared socioeconomic pathway used for post-`projection_year` bleaching severity, coral growth `pH` stress (`sqrt(SSP)`), and related scenario branches. Supported values in code: `1.9`, `2.6`, `4.5`, `7.0`, `8.5`. |
| Ensemble runs | `ensemble_runs` | Number of stochastic ensemble replicates (`1..ensemble_runs`); ensemble `0` is spin-up. |
| Start year | `start_year` | Calendar year when saved initial conditions are loaded for ensembles `>= 1`. |
| Spinup backtrack (years) | `spinup_backtrack_years` | For ensemble `0` only, simulation begins at `start_year - spinup_backtrack_years` to warm up the system. |
| Save year | `save_year` | Initial state for ensembles `>= 1` is taken from the end of the spin-up trajectory in the year before `save_year` (see model loop). |
| Projection year | `projection_year` | Before this year, historical cyclone/bleaching drivers apply where coded; from this year onward, scenario-based projection drivers apply. |
| End year | `end_year` | Last calendar year in the main annual loop. |
| Search year | `search_year` | When `search_mode == 1`, used to switch search behaviour and benefit accumulation (optimization-style mode; default `search_mode` is `0`). |

### Crown-of-thorns (CoTS) control

All CoTS control logic runs only when `year >= start_CoTS_control`. Vessel counts set how many control “vessels” are simulated for that annual step; **each non-zero regional/GBR/sector option runs its own control pass** in sequence (separate dive budgets). Intervention reefs are still filtered by region for the regional modes.

| Legacy CSV label | `CoconetConfig` field | Role |
| --- | --- | --- |
| CoTS control start year | `start_CoTS_control` | First year any CoTS control runs. |
| CoTS control ecological threshold (CoTS per ha) | `eco_threshold` | Site-level CoTS density above which culling dives target that site; also scales post-cull age-class distribution. |
| CoTS control CoTS threshold (CoTS per ha) | `CoTS_threshold` | Reef-level mean CoTS (`S_manta_r`) must be **below** this (or coral above `coral_threshold`) for the reef to be eligible for control. |
| CoTS control coral threshold (CoTS per ha) | `coral_threshold` | If reef mean coral cover exceeds this, the reef is eligible even when CoTS density is high (legacy naming says “CoTS per ha” but the field is compared to coral cover). |
| CoTS vessels across GBR | `CoTS_vessels_GBR` | Sets vessel count and `control_region = "GBR"` for `control_cots()`. |
| CoTS vessels in Far-northern Region | `CoTS_vessels_FN` | Same for region `FN`. |
| CoTS vessels in Northern Region | `CoTS_vessels_N` | Same for region `N`. |
| CoTS vessels in Central Region | `CoTS_vessels_C` | Same for region `C`. |
| CoTS vessels in Southern Region | `CoTS_vessels_S` | Same for region `S`. |
| CoTS vessels in active sector | `CoTS_vessels_sector` | Vessel count for `control_cots_by_sector()`: targets the sector (1–11) with highest mean CoTS; uses a slightly different dive-cost formula than regional control. |

### Catchment, zoning, and fishing

| Legacy CSV label | `CoconetConfig` field | Role |
| --- | --- | --- |
| Catchment restoration start year | `start_catchment_restore` | First year catchment recovery can proceed. |
| Catchment restoration timescale (years) | `restore_timeframe` | Each year, `catchment_condition` moves toward `1` by `1/restore_timeframe` (exponential approach). Must be `> 0` for the update to run. Used in cyclone/flood loading (`flood_load`). |
| Future zoning start year | `start_modified_zoning` | Year assigned as `future_rezone_year` for the first `rezoned_reefs` reefs in priority order (among reefs that still have `rezone_year == 9999`). |
| Number of reefs included in future rezoning | `rezoned_reefs` | Count of reefs that receive a planned future closure year. |
| Reduction in fisheries catch start year | `start_modified_fishing` | From this year on, global annual emperor and coral-trout catch caps are multiplied by `1 - catch_reduction`. |
| Fractional reduction in fisheries catches | `catch_reduction` | Fractional cut applied to `e_annual` and `g_annual` (see `apply_fishing`). |
| Upper fish size limit start year | `start_upper_sizelimit` | From this year, fishing removes no `E_5` / `G_5` (largest age classes). |
| Lower fish size limit start year | `start_lower_sizelimit` | From this year, fishing removes no `E_3` / `G_3` (smallest targeted ages). |
| Exclude fishing from active outbreak reefs start year | `start_CoTSlimit` | From this year, if a weighted CoTS outbreak index on the reef exceeds `68`, all targeted emperor/trout catch on that visit is set to zero. |

### Other species and shading

| Legacy CSV label | `CoconetConfig` field | Role |
| --- | --- | --- |
| Emperor release start year | `start_emperor_release` | First year `release_fish()` runs. |
| Number of release reefs | `release_reefs` | Maximum reefs to stock per year (priority order, within intervention lat/lon box). |
| Maximum adult emperors (per ha) for release | `release_threshold` | Reefs at or above this total adult emperor biomass are **skipped** (release targets depleted reefs). |
| Number of juvenile emperors released per reef | `release_number` | Juveniles added to `E_1`, spread per reef area (ha). |
| Regional shading start year | `start_regional_shading` | First year `shade_regional_coral()` runs. |
| Absolute DHW reduction due to regional shading (DHW) | `regional_shading_reduction` | For reefs inside the intervention bounding box, bleaching DHW is multiplied by `(1 - regional_shading[reef])` after this value is assigned (same multiplicative pattern as local shading; legacy label says “absolute” but the implementation scales DHW). |

### Intervention bounding box

Applied to most spatial interventions (not CoTS regional control, which uses reef region codes). Reef centre must satisfy `intervene_lon_min < x < intervene_lon_max` and `intervene_lat_min < y < intervene_lat_max`.

| Legacy CSV label | `CoconetConfig` field |
| --- | --- |
| Minimum longitude of interventions | `intervene_lon_min` |
| Maximum longitude of interventions | `intervene_lon_max` |
| Minimum latitude of interventions | `intervene_lat_min` |
| Maximum latitude of interventions | `intervene_lat_max` |

### Rubble, coral seeding, slicks, local shading, ocean acidification

| Legacy CSV label | `CoconetConfig` field | Role |
| --- | --- | --- |
| Rubble consolidation start year | `start_rubble_consolidation` | Enables `consolidate_rubble()`. |
| Annual number of consolidated reefs | `consolidation_reefs` | Cap on reefs treated per year. |
| Minimum rubble cover threshold for consolidation [0 1] | `consolidation_threshold` | Reef mean rubble must be **strictly greater** than this to qualify. |
| Total annual consolidated area (ha) | `consolidation_hectares` | Divided by `ha_per_site` to give per-site rubble removal at a chosen site. |
| Thermally tolerant coral seeding start year | `start_coral_seeding` | Enables `seed_tt_coral()`. |
| Annual number of reefs seeded with coral | `seed_reefs` | Max reefs per year. |
| Maximum coral cover threshold for coral seeding [0 1] | `seed_threshold` | Reefs at or above this mean coral cover are **skipped**. |
| Total annual area of seeded corals (ha) | `seed_hectares` | Determines fractional cover added per seeding event. |
| Fraction of staghorn … hybridise … [0 1] | `hybrid_fraction` | In `spawn_corals()`, scales hybridisation of branching (`sa`) with thermally tolerant (`tt`) corals for recruitment composition and thermal traits. |
| Dominance of thermally tolerant corals … [0 1] | `dominance` | Weights how hybrid thermal tolerance blends toward `tt` vs `sa` in the same hybrid calculation. |
| Coral slicks start year | `start_coral_slick` | Enables `seed_coral_slick()`. |
| Annual number of reefs with coral slicks released | `slick_reefs` | Max reefs per year. |
| Maximum coral cover threshold for coral slicks [0 1] | `slick_threshold` | Reefs with thermally tolerant reef-mean cover `C_r["tt"]` **at or above** this are **skipped**. |
| Total annual area of slick corals (ha) | `slick_hectares` | Slick fraction per site; material is drawn from a random source site’s community composition. |
| Reef shading start year | `start_reef_shading` | Enables `shade_local_reef()`. |
| Annual number of reefs locally shaded | `shading_reefs` | Each chosen reef gets `reef_shading[reef] = reef_shading_reduction`. |
| Fractional DHW reduction due to local shading [0 1] | `reef_shading_reduction` | Bleaching DHW multiplied by `(1 - reef_shading[reef])`. |
| Ocean acidification treatment start year | `start_pH_protection` | Enables `increase_pH()`. |
| Annual number of reefs treated for ocean acidification | `pH_reefs` | Reefs per year receive `pH_protect[reef] = pH_protection`. |
| Fractional protection from ocean acidification [0 1] | `pH_protection` | From `projection_year` onward, coral growth uses `pH_effect_t = (1 - pH_protect[reef]) * sqrt(SSP)`. |

### Search mode and “perfect” interventions

- **`search_mode`** (`0` default): normal output; `1` enables search-year logic and benefit tracking (see `model.py`).
- **`perfect_intervention`**: string mode applied from year **2026** onward (`_apply_perfect_intervention`), e.g. idealised starfish control or shading (not set via legacy CSV).
- **`unregulated_fishing`**: if true, sets zoning years so fishing regulation drivers are bypassed.

For the exact equations, see the corresponding methods on `CoconetModel` in `coconet/model.py` and the label map in `coconet/config.py`.
