Skip to content

Commit f55d1dd

Browse files
authored
start to HABs scenario (#64)
* have to run off OpenDrift branch for now * fixed a bug for running backward in time * added a new parameter to the ocean model configuration files * updated tests and docs
1 parent ef76422 commit f55d1dd

30 files changed

+603
-26
lines changed

ci/environment-py3.11.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- kerchunk ==0.2.7
1515
- xarray
1616
- numpy <=1.26.4 # req from opendrift
17-
- opendrift ==1.13.0
17+
# - opendrift ==1.13.0
1818
- scipy
1919
- dask
2020
- netcdf4
@@ -31,8 +31,8 @@ dependencies:
3131
- coverage
3232
- pip
3333
- pytest
34-
# - pip:
34+
- pip:
3535
# - adios_db
36-
# - git+https://github.com/OpenDrift/opendrift
36+
- git+https://github.com/kthyng/opendrift.git@start_habs
3737
# - git+https://github.com/fsspec/kerchunk
3838
# - xroms # can't be found on conda-forge for CI, but don't need since in runslow tests?

ci/environment-py3.12.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- kerchunk ==0.2.7
1515
- xarray
1616
- numpy <=1.26.4 # req from opendrift
17-
- opendrift ==1.13.0
17+
# - opendrift ==1.13.0
1818
- scipy
1919
- dask
2020
- netcdf4
@@ -31,7 +31,8 @@ dependencies:
3131
- coverage
3232
- pip
3333
- pytest
34-
# - pip:
34+
- pip:
35+
- git+https://github.com/kthyng/opendrift.git@start_habs
3536
# - adios_db
3637
# - git+https://github.com/OpenDrift/opendrift
3738
# - git+https://github.com/fsspec/kerchunk

ci/environment-py3.13.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- kerchunk ==0.2.7
1515
- xarray
1616
- numpy <=1.26.4 # req from opendrift
17-
- opendrift ==1.13.0
17+
# - opendrift ==1.13.0
1818
- scipy
1919
- dask
2020
- netcdf4
@@ -31,7 +31,8 @@ dependencies:
3131
- coverage
3232
- pip
3333
- pytest
34-
# - pip:
34+
- pip:
35+
- git+https://github.com/kthyng/opendrift.git@start_habs
3536
# - adios_db
3637
# - git+https://github.com/OpenDrift/opendrift
3738
# - git+https://github.com/fsspec/kerchunk

docs/conf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
"myst_nb",
6363
]
6464

65+
myst_enable_extensions = [
66+
"dollarmath", # enables $...$ and $$...$$
67+
"amsmath", # enables \begin{equation}, \text{}, etc.
68+
]
69+
6570
# for compiling notebooks with mystnb
6671
# https://docs.readthedocs.io/en/stable/guides/jupyter.html#using-notebooks-in-other-formats
6772
nb_custom_formats = {

docs/configuration.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ A handful of `pydantic` BaseModels make up the configuration for PTM. This allow
1919

2020
The main configuration classes are:
2121
1. `TheManagerConfig`
22-
1. `OpenDriftConfig` and instances `LarvalFishModelConfig`, `LeewayModelConfig`, `OceanDriftModelConfig`, `OpenOilModelConfig`
22+
1. `OpenDriftConfig` and instances `LarvalFishModelConfig`, `LeewayModelConfig`, `OceanDriftModelConfig`, `OpenOilModelConfig`, `HarmfulAlgalBloomModelConfig`
2323

2424
Other configuration classes are:
2525
1. `OceanModelConfig`
@@ -38,35 +38,47 @@ To retrieve the JSON schema for most parameters related to a PTM run, you can ac
3838
```{code-cell} ipython3
3939
import particle_tracking_manager as ptm
4040
import pprint
41+
import json
4142
42-
pprint.pprint(ptm.OceanDriftModelConfig.model_json_schema())
43+
schema = ptm.OceanDriftModelConfig.model_json_schema()
44+
print(json.dumps(schema, indent=2))
4345
```
4446

4547
#### LarvalFishModelConfig
4648

4749
```{code-cell} ipython3
48-
pprint.pprint(ptm.LarvalFishModelConfig.model_json_schema())
50+
schema = ptm.LarvalFishModelConfig.model_json_schema()
51+
print(json.dumps(schema, indent=2))
4952
```
5053

5154
#### LeewayModelConfig
5255

5356
```{code-cell} ipython3
54-
pprint.pprint(ptm.LeewayModelConfig.model_json_schema())
57+
schema = ptm.LeewayModelConfig.model_json_schema()
58+
print(json.dumps(schema, indent=2))
5559
```
5660

5761
#### OpenOilModelConfig
5862

5963
```{code-cell} ipython3
60-
pprint.pprint(ptm.OpenOilModelConfig.model_json_schema())
64+
schema = ptm.OpenOilModelConfig.model_json_schema()
65+
print(json.dumps(schema, indent=2))
6166
```
6267

68+
#### HarmfulAlgalBloomModelConfig
6369

64-
#### TheManageConfig
70+
```{code-cell} ipython3
71+
schema = ptm.HarmfulAlgalBloomModelConfig.model_json_schema()
72+
print(json.dumps(schema, indent=2))
73+
```
74+
75+
#### TheManagerConfig
6576

6677
You can also examine the schema for `TheManagerConfig` directly, which is a subset of the parameters in the scenario classes (which inherit from `TheManagerConfig`).
6778

6879
```{code-cell} ipython3
69-
pprint.pprint(ptm.TheManagerConfig.model_json_schema())
80+
schema = ptm.TheManagerConfig.model_json_schema()
81+
print(json.dumps(schema, indent=2))
7082
```
7183

7284

@@ -86,6 +98,7 @@ Though `OpenDrift` has more models available, the currently wrapped `drift_model
8698
* Leeway: scenario for Search and Rescue of various objects at the surface
8799
* OpenOil: oil spill scenarios
88100
* LarvalFish: scenario for fish eggs and larvae that can grow
101+
* HarmfulAlgalBloom: scenario for modeling harmful algal blooms once they exist to see where they travel or where they came from
89102

90103
Set these with e.g.:
91104

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ To install from PyPI:
2525
quick_start
2626
tutorial
2727
ocean_models
28+
scenario_details
2829
configuration
2930
plots
3031

docs/ocean_models.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ Here we describe how to user existing ocean model configurations as well as how
2020
Some ocean models are built into PTM and can be accessed by the input parameter `ocean_model`.
2121

2222
The built-in ocean models are:
23-
* **NWGOA** (1999–2008) over the Northwest Gulf of Alaska (Danielson, S. L., K. S. Hedstrom, E. Curchitser, 2016. Cook Inlet Model Calculations, Final Report to Bureau of Ocean Energy Management, M14AC00014, OCS Study BOEM 2015-050, University of Alaska Fairbanks, Fairbanks, AK, 149 pp.)
23+
* **CIOFS3** (1999–2024) across Cook Inlet, Alaska, the newest hindcast version of NOAA's CIOFS model. (Thyng, K. M., C. Liu, 2025. Cook Inlet Circulation Modeling - Long-term Hindcast with Improved Freshwater Forcing and Other Attributes, Final Report to the National Oceanic Atmospheric Administration National Centers for Coastal Ocean Science Kasitsna Bay Lab, Axiom Data Science, Anchorage, AK.)
2424
* **CIOFS** (1999–2022) across Cook Inlet, Alaska, a hindcast version of NOAA's CIOFS model. (Thyng, K. M., C. Liu, M. Feen, E. L. Dobbins, 2023. Cook Inlet Circulation Modeling, Final Report to Oil Spill Recovery Institute, Axiom Data Science, Anchorage, AK.)
2525
* **CIOFSOP** (mid-2021 through 48 hours from present time) which is the nowcast/forecast version of the CIOFS model. (Shi, L., L. Lanerolle, Y. Chen, D. Cao, R. Patchen, A. Zhang,
2626
and E. P. Myers, 2020. NOS Cook Inlet Operational Forecast System: Model development and hindcast skill assessment, NOAA Technical Report NOS CS 40, Silver Spring, Maryland, September 2020.)
27+
* **NWGOA** (1999–2008) over the Northwest Gulf of Alaska (Danielson, S. L., K. S. Hedstrom, E. Curchitser, 2016. Cook Inlet Model Calculations, Final Report to Bureau of Ocean Energy Management, M14AC00014, OCS Study BOEM 2015-050, University of Alaska Fairbanks, Fairbanks, AK, 149 pp.)
2728

2829

2930
### Show available ocean models
@@ -41,7 +42,7 @@ ocean_model_registry.all()
4142
Show each individual ocean_model:
4243

4344
```{code-cell} ipython3
44-
ocean_model_registry.show("NWGOA")
45+
ocean_model_registry.show("CIOFS3")
4546
```
4647

4748
```{code-cell} ipython3
@@ -52,6 +53,10 @@ ocean_model_registry.show("CIOFS")
5253
ocean_model_registry.show("CIOFSOP")
5354
```
5455

56+
```{code-cell} ipython3
57+
ocean_model_registry.show("NWGOA")
58+
```
59+
5560
### Return ocean model object
5661

5762
To instead return the ocean model config object, use `get`:

docs/scenario_details.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Details about individual drift models
2+
3+
## Harmful Algal Blooms
4+
5+
With the `HarmfulAlgalBloom` model, users can transport an existing bloom forward in time or run backward in time to determine where it originated.
6+
7+
Currently users can model *Pseudo nitzschia* as a near-surface-limited bloom that experiences high mortality when outside viable temperature and salinity ranges. No growth is currently included. This is the start to tracking relative biomass of the particles making up the bloom.
8+
9+
### Biomass and Mortality Framework
10+
11+
The model includes a simple habitat-dependent survival model in which temperature and salinity determine mortality, and biomass evolves according to an exponential decay equation.
12+
13+
#### Environmental Zone Classification
14+
15+
For each particle, the model first evaluates its local temperature and salinity to determine whether environmental conditions fall within a **preferred**, **marginal**, or **lethal** range. This classification is handled by `classify_zone`, which assigns each element to one of:
16+
17+
- `baseline_mortality` (preferred habitat)
18+
- `medium_mortality` (suboptimal but viable habitat)
19+
- `high_mortality` (outside survival limits)
20+
21+
Temperature classification uses four thresholds:
22+
23+
- `temperature_pref_min`
24+
- `temperature_pref_max`
25+
- `temperature_death_min`
26+
- `temperature_death_max`
27+
28+
Salinity classification uses an analogous set:
29+
30+
- `salinity_pref_min`
31+
- `salinity_pref_max`
32+
- `salinity_death_min`
33+
- `salinity_death_max`
34+
35+
36+
The result is a per-particle assessment of environmental stress.
37+
38+
#### Mortality Rate Selection
39+
40+
The model then assigns each particle a mortality rate according to a tiered decision rule implemented in `choose_mortality_rate`:
41+
42+
1. If **temperature OR salinity** is in a `high_mortality` zone →
43+
use `mortality_rate_high`.
44+
45+
2. Else, if **either** variable is in a `medium_mortality` zone
46+
(and neither is high) →
47+
use `mortality_rate_medium`.
48+
49+
3. Only when **both** temperature and salinity lie in preferred ranges →
50+
use `mortality_rate_baseline`.
51+
52+
This "worst-condition wins" approach prevents double-counting stress while ensuring that the most limiting factor controls mortality.
53+
54+
The output is an array `mortality_rates` (units: days$^{-1}$).
55+
56+
#### Biomass Evolution
57+
58+
Biomass is updated using an exponential decay formulation. Growth is not yet implemented, so the net rate is purely negative:
59+
60+
61+
This corresponds to integrating:
62+
63+
$$
64+
\frac{dB}{dt} = (\text{growth_rate} - \text{mortality_rate}) \times B,
65+
$$
66+
67+
so biomass decays on a timescale determined by the assigned mortality rate. Particles in lethal zones decay fastest; particles in preferred zones decay slowly (not yet implemented). The solution to the equation is
68+
69+
$$
70+
B(t) = B(0) \exp^{(\text{growth_rate} - \text{mortality_rate})t}
71+
$$
72+
73+
#### Deactivation of Dead Particles
74+
75+
Once biomass is updated, particles whose biomass falls below the threshold `biomass_dead_threshold` are removed from the simulation (or deactivated). Deactivated particles no longer participate in advection, mixing, or further biological updates, allowing the Lagrangian population to thin naturally in unfavorable environments.
76+
77+
78+
79+
### Next steps:
80+
* add growth
81+
* add medium level and baseline mortality
82+
* add Alexandrium catenella and Dinophysis spp.
83+
* add vertical behavior framework - Implement a shared parameterized vertical-movement function (band / diel_band)
84+
* Pseudo-nitzschia: shallow band
85+
* Alexandrium: diel vertical migration (shallow band by day, deeper band by night)
86+
* Dinophysis: mid-depth band (around the pycnocline)

docs/tutorial.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,67 @@ m.run_all()
5353
```
5454

5555

56+
## Harmful Algal Bloom
57+
58+
The goal of this scenario is to examine the transport of an existing harmful algal bloom or determine where an existing bloom originated, respecting temperature and salinity bounds for the species.
59+
60+
### Initialize manager `m`
61+
62+
```{code-cell} ipython3
63+
m = ptm.OpenDriftModel(drift_model="HarmfulAlgalBloom", lon = -89.8, lat = 29.08,
64+
number=10, steps=40,
65+
ocean_model="TXLA",
66+
start_time="2009-11-19T12:00",
67+
ocean_model_local=False,
68+
species_type="Pseudo_nitzschia",
69+
plots={'spaghetti': {}})
70+
```
71+
72+
The currently available species are:
73+
74+
```{code-cell} ipython3
75+
ptm.HarmfulAlgalBloomModelConfig.model_json_schema()["$defs"]["HABSpeciesTypeEnum"]["enum"]
76+
```
77+
78+
where `custom` is an option to allow the user to input all necessary parameters to represent a species of their choice.
79+
80+
There are parameters available just for the HAB model:
81+
82+
```{code-cell} ipython3
83+
import json
84+
print(json.dumps(ptm.models.opendrift.enums.species_types.HABParameters.model_json_schema(), indent=2))
85+
```
86+
87+
88+
The special parameters for each available species are:
89+
90+
```{code-cell} ipython3
91+
species = ptm.HarmfulAlgalBloomModelConfig.model_json_schema()["$defs"]["HABSpeciesTypeEnum"]["enum"]
92+
species.remove('custom')
93+
for specie in species:
94+
print(f"{specie}: {ptm.models.opendrift.enums.species_types.SPECIES_HAB_DEFAULTS[specie]}")
95+
```
96+
97+
The regular parameters that are set for each available species are:
98+
99+
```{code-cell} ipython3
100+
for specie in species:
101+
print(f"{specie}: {ptm.models.opendrift.enums.species_types.SPECIES_HAB_MANAGER_DEFAULTS[specie]}")
102+
```
103+
104+
The configuration parameters for this simulation are:
105+
106+
```{code-cell} ipython3
107+
pprint.pprint(m.config.model_dump())
108+
```
109+
110+
### Run
111+
112+
```{code-cell} ipython3
113+
m.run_all()
114+
```
115+
116+
56117
## Leeway (Search and Rescue)
57118

58119
These are simulations of objects that stay at the surface and are transported by both the wind and ocean currents at rates that depend on how much the object sticks up out of and down into the water. The constants to use for those rates have been experimentally determined by the coastguard and are used in this model.

docs/whats_new.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# What's New
22

3+
## Unreleased
4+
5+
* Added a new model scenario: `HarmfalAlgalBloom`.
6+
* Fix a bug for running backward in time.
7+
* Ocean model configuration files now need to include `chunks`
8+
39
## v0.13.2 (November 20, 2025)
410

511
* If timezone-aware `start_time`, `start_time_end`, or `end_time` is input, the timezone is now removed and it is assumed they are in the same timezone as the model times.

0 commit comments

Comments
 (0)