Contents

  1. What is CAPTAIN? Background on machine learning and key features
  2. Getting started
  3. Package architecture
  4. Input data
  5. Features & toggles
  6. Parameters
  7. Example UGF Case Study
  8. Resources

Introduction to Conservation Prioritization using CAPTAIN

Captain is a spatial prioritization tool for conservation and restoration planning using artificial intelligence.

HOW IT WORKS: It does not create one “optimal” static map the way a solver does.

It trains an AI neural network model to learn a policy - a decision rule book - for protecting cells in a simulated landscape as it evolves, in order to optimize conservation strategies in space and time.

This is done through reinforcement-learning (RL), a type of machine learning process where AI “agents” learn to make decisions by interacting with their environment.

CAPTAIN workflow for minimizing biodiversity loss

Introduction to Conservation Prioritization using CAPTAIN

CAPTAIN uses AI Reinforcement Learning (RL) to train a model for area prioritization that achieves a conservation goal (e.g., minimizing species loss) based on empirically informed ecological simulations:

1. Simulates a landscape through time (e.g., species populations grow, disperse, and are affected by disturbance and climate).

  1. At each timestep, the neural network (“policy”) scores every grid cell for how good it would be to protect.

  2. Cells are protected within a budget, the simulation steps forward, and a reward is computed (e.g., reduced extinction risk, cost efficiency).

  3. Training repeatedly perturbs the policy’s weights, re-runs episodes, and nudges the weights toward higher reward. This is known as Evolution Strategies.

Creator Daniele Silvestro explaining the CAPTAIN framework

Key CAPTAIN v.3 features

CAPTAIN v.3 is in active development, but its core features are functional. Builds upon previous CAPTAIN v.2 published at (Silvestro et al. 2022).

                
                

⚙️ Engineered for Scale: Full GPU support and optimized performance for analysis at larger scales and finer resolutions.

🌱 Dynamic Environment Scenarios: Time-evolving scenarios, incorporating climate change projections and dynamic implementation costs.

🧩 Modular & Customizable: Tailor conservation policies, features, rewards and input custom spatial data.

🎯 Multi-objective Optimization: Quanitfy trade-offs between competing conservation and restoration targets.

🤖 Decentralized Regional Agents: Coordinated, region-specific policies

Machine Learning terms used

Term Plain-language meaning
Epoch One round of testing updated policy weights (N_EPOCHS)
Evolution Strategies (ES) The training method used. Perturbs weights randomly, keeps what scores higher
Perturbation / population size How many tweaked policy copies are tested per epoch (N_PERTURBATIONS)
Policy (network) Decision rule book strategy for how each cells protection worth is scored
Reward Value used to judge a policy based on outcome(e.g., lower extinction risk, lower cost)
Feature One piece of per-cell info fed to the policy
Episode One full simulated run through time (N_TIME_STEPS)
Budget manager Enforces how many cells can be protected, and when

Getting Started

Choose an IDE (Integrated Development Environment)

Similar to RStudio for R, this is an editor where you will edit python code and run scripts in the terminal.

Positron (recommended): A free IDE made specifically for analysis with Python and R. Created by Posit, the makers of RStudio and is R user friendly:

1. Install Python via uv

uv is a Python package/environment manager, which can download and manage its own Python versions automatically, instead of plain pip/conda.

In a terminal:

macOS / Linux

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell)

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

2. Set up the environment

Once uv is installed, clone the repository and sync the dependencies. uv sync will automatically create a virtual environment (.venv) and install the correct version of PyTorch.

# Clone and enter the repository
git clone https://github.com/captain-project/captain3preview
cd captain3preview
# Sync dependencies and create virtual environment
uv sync

3. Verify the install

>>> import captain as cn
>>> cn.__version__

Project structure

Directory of files stored inside the captain project folder. Users only need to edit example files (next slide)

captain3preview/
├── captain/            # Main package
│   ├── agents/         # Policy network, feature extraction, rewards
│   ├── algorithms/     # Evolution strategies trainer, episode runner
│   ├── data/           # SpatialData, ExtinctionRisk classes
│   ├── environment/    # BioEnv simulation engine
│   └── utils/          # Utilities, data loading
└── examples/           # train_policy.py, run_inference.py, ...

Captain model sequence: DataLoaderSpatialDataBioEnvFeatureExtractorPolicyNetworkRewardsEpisodeRunnerEvolStrategiesTrainer

Main CAPTAIN Scripts (.py) in order of when to run them

These are the scripts you edit in order to train and run the model.

Run order: calibrate_rewards.pytrain_policy.pyrun_inference.py

Purpose

plot_input_data.py Plots to visualize raw/time-evolving inputs (optional)
calibrate_rewards.py

Run once before training a model to calibrate reward-term scales and saves reward_calibration.json.

Keeps reward values comparable across global/regional/single-region runs). Re-run only if the reward terms, weights, or datasets change.)

train_policy.py Trains a policy with Evolution Strategies (ES), loads input data files and the saved reward calibration json file.
run_inference.py Loads a trained policy (train_policy.py) and runs one episode, producing protection maps and extinction-risk plots.
#terminal
uv run calibrate_rewards.py    # once, to generate reward_calibration.json
uv run train_policy.py         # Model training using empirical data inputs
uv run run_inference.py.       # Policy inference plots using trained.py output

Other: Internal Captain model scripts

These are the internal Captain scripts that process the data and run the model based on the input settings from your train_policy and run_inference .py scripts.

See more details in https://github.com/captain-project/captain3preview/blob/main/docs/architecture.md

File Role
bioenv.py BioEnv — the simulation engine
spatial_data.py SpatialData — gridded layers, time evolution
extinction_risk.py ExtinctionRisk — IUCN-style risk classification
feature_extractor.py Builds/normalizes the policy’s observations
policy_network.py CellNN + PolicyNetwork/RegionalPolicyNetwork

rewards.py /

reward_aggregator.py

Individual reward terms + weighted combo
evolution_train.py EvolStrategiesTrainer — the ES loop
episode.py EpisodeRunner — orchestrates one episode
budget_manager.py Global / Regional / No budget managers
scheduler.py LearningScheduler — decays alpha/sigma

1. Required Inputs: Empirical Data

Data Directory = DATA_DIR

Input What it is Variable
SDMs (present) Habitat suitability raster per species, 0–1 PRESENT_SDMS_DIR
SDMs (future) Same, projected forward (optional) FUTURE_SDMS_DIR
Species trait table CSV, one row/species SPECIES_TRAIT_FILE
Disturbance layer GeoTIFF, present (+future) DISTURBANCE_FILE
Cost layer GeoTIFF, present (+future) COST_FILE
Area mask .npy, valid=1.0, invalid=NaN mask including only the valid cells for the analysis DATA_MASK Can build with captains cn.data_loader.create_mask_from_map()

1. a) Trait table columns (species_traits.csv)

Column Used for
species Must match SDM (present/ future) raster file names
growth_rate Per-timestep population growth rate.
sensitivity_disturbance 0–1, how strongly the species’ mortality responds to the disturbance layer.
conservation_status 1–5 IUCN-style code
dispersal_ability 0-1, Per-species dispersal rate
min_habitat_suitability Per-species minimum habitat-suitability threshold. Cells below the threshold don’t contribute to that species’ carrying capacity.
Carrying_capacity (optional add in) Empirical per species per cell carrying capacity values

2. Training toggles: Feature set — what the policy “sees”

In train_policy.py, feature_set=None uses this full default_feature_set. List much match between Training & Inference.
# feature_extractor.py — default_feature_set
[
    "time", "disturbance", "disturbance_conv", "species_richness",
    "total_population", "current_ext_risk", "cost",
    "protection_matrix", "protection_matrix_conv",
]

3. Reward objectives

# From train_policy.py
reward_obj_list = [
    cn.CalcRewardExtRisk(threat_weights=np.array([1, 0, -8, -16, -32]), device=DEVICE),
]
if INCLUDE_COST:
    reward_obj_list.append(
        cn.CalcRewardPersistentCost(rescaler=float(1.0 / costs.data.sum()))
    )
rewards = cn.Rewards(reward_obj_list=reward_obj_list, reward_weights=REWARD_WEIGHTS)

Each entry in reward_obj_list is one objective; reward_weights sets how they’re combined. This is CAPTAIN’s multi-objective optimization feature

threat_weights penalizes by IUCN category: [LC=1, NT=0, VU=-8, EN=-16, CR=-32]

4. Training loop parameters

Parameter Role ↑ effect ↓ effect Default in scripts
N_EPOCHS ES iterations Better-trained, slower Faster, less converged 20
N_PERTURBATIONS Population size/epoch Stabler estimate, more compute Noisier, seed-variance 6
N_TIME_STEPS Episode length Longer-horizon dynamics Cheaper, may truncate 50
TARGET_PROTECTED_CELLS_FRACTION Protection target Larger footprint Smaller footprint 0.10
CELLS_PER_STEP Cells/timestep Faster rollout Gradual rollout 1000

5. Species / ecological parameters

Parameter Role Default
DISPERSAL_RATE Legacy scalar control 0.5 — obsolete if dispersal_ability is used
DISPERSAL_WINDOW Dispersal neighborhood radius 3
MIN_HABITAT_SUITABILITY Presence threshold 0.5 in your scripts
ExtinctionRisk(n_classes=5, alpha=0.5) 5 IUCN classes alpha shapes thresholds

Example Analysis: UGF Case Study

This example tutorial walks through training and evaluating a biodiversity conservation policy with CAPTAIN using 18 species over a grid of 135,830 cells in the UGF boundary of West Africa.

Scripts

UGF_calibrate_rewards.py
      │  creates reward_calibration.json. Run once before training
      ▼
UGF_train_policy.py
      │  trains policy → trained_weights.npy
      ▼
UGF_run_inference.py  + (GENERATE MAPS)
      │  runs policy once → <trained policy>_log.tsv + reward_over_training.png 
      ▼
Output Protection Policy

Step 1 - Prepare your empirical data

Assemble the empirical data inputs from Section 1, matching the directory/file structure the loader functions expect (present/future SDM directories, present/future disturbance and cost GeoTIFFs, trait CSV, area mask). Update DATA_DIR and the filename constants at the top of each script to point at your data.

# CONFIG — DATA PATHS
# =============================================================================
DATA_DIR = Path("/Users/captain3preview/ugf3_data")  # <-- change this

Ex: Species trait table column headers

species_traits.csv

Spatial data layers

Composite AOH for 18 bird species in the Upper Guinean Forest (Lumbierres et al., 2022) weighted by extinction risk.

Established protected areas in the UGF by category, derived from UNEP-WCMC and IUCN, 2026.

Present day disturbance levels (2022) in the Upper Guinean Forest, derived from Theobald et al., 2025.

Opportunity cost for protection in the Upper Guinean Forest, represented as cocoa crop suitability for rainfed and irrigated crop land, derived from Zabel et al., 2024.

Step 2 - Experiment Toggles

Edited scripts (Ugf_*) wrap the examples in explicit toggles, each labeled [TOGGLE: ...] in the code:

Toggle Options Effect
SENSITIVITY_MODE "empirical" / "flat" Trait-based vs. constant sensitivity
INCLUDE_COST True / False Adds cost to reward and features
INCLUDE_FUTURE True / False Turns on time-evolving layers
INCLUDE_EXISTING_PROTECTED_AREAS True / False Start from real PA raster
USE_REGIONAL_AGENTS True / False Individual agents per region vs. single global agent

Toggles must match exactly between training and inference. Mismatch will silently change feature_extractor.n_features and errors or misload weights.

Toggle example

# From UGF_train_policy.py
if SENSITIVITY_MODE == "empirical":
    sensitivity = traits["sensitivity_disturbance"].to_numpy(copy=True)[:, np.newaxis]
elif SENSITIVITY_MODE == "flat":
    sensitivity = np.full((sdm.shape[0], 1), FLAT_SENSITIVITY_VALUE, dtype=np.float32)

Step 3 - Configure Parameters

Edit parameters where necessary based on analysis testing for policy goal (or leave at default)

# Ex from UGF_train_policy.py
# =============================================================================
# CONFIG — TRAINING / POLICY PARAMETERS

N_EPOCHS = 100  # Number of training iterations
N_PERTURBATIONS = 6  # Number of parallel episode evaluations (sequential on GPU)

# Ex from UGF_train_policy.py
# =============================================================================
# CONFIG — SPECIES / DISPERSAL PARAMETERS

DISPERSAL_RATE = 0.5  # can be an array (per-species values)
DISPERSAL_WINDOW = 3

Step 4 - Run Training

#terminal 

# run calibrate rewards.py (run once per reward configuration & reuse for subsequent runs)
uv run UGF_calibrate_rewards.py  # only run again if reward_obj_list changes.

# then run train_policy.py
uv run UGF_train_policy.py      # Model training using empirical data inputs

Step 5 - Run Inference / apply trained policy

Rebuild the same setup as training in run_inference.py (must match the architecture the weights were trained with).

Match the same SEED and CONFIG toggle values (e.g., NUMBER_TIME_STEPS, INCLUDE_FUTURE, USE_REGIONAL_AGENTS) from the training script.


#Ex from UGF_run_inference.py 
SEED = # <-- same as training!

# =============================================================================
# CONFIG — DATA PATHS
# (must match the training run's config exactly)

DATA_DIR = Path("/Users/kalena/captain3preview/ugf3_data")  # <-- change this
OUTPUTS_DIR = Path("/Users/kalena/captain3preview/outputs")

#...

#-------- future scenario layers (optional) -----------------------------------#
# [TOGGLE: FUTURE] Must match whatever the training run used. If these stay
# None, delta_sdm/delta stays None and the layer doesn't change over time (present-day-only scenario)
FUTURE_SDMS_DIR = None                    # e.g. "future_sdms"
FUTURE_DISTURBANCE_FILE = None         # e.g."environmental_layers/future_disturbance.tif"
FUTURE_COST_FILE = None                   # e.g. "environmental_layers/future_cost.tif"

Then run:

#terminal 
uv run UGF_run_inference.py 

Resources