Skip to content

Commit

Permalink
Merge pull request #24 from kthyng/updates
Browse files Browse the repository at this point in the history
improved drift_model_config(), include PTM metadata with output file
  • Loading branch information
kthyng authored Apr 11, 2024
2 parents 818cbde + 967995b commit 1f8ffb7
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 38 deletions.
8 changes: 7 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "**.ipynb_checkpoints", "Thumbs.db", ".DS_Store"]
exclude_patterns = [
"*.ipynb",
"_build",
"**.ipynb_checkpoints",
"Thumbs.db",
".DS_Store",
]


# -- Options for HTML output -------------------------------------------------
Expand Down
10 changes: 5 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ import xroms
m = ptm.OpenDriftModel(lon=-90, lat=28.7, number=1, steps=2)
url = xroms.datasets.CLOVER.fetch("ROMS_example_full_grid.nc")
reader_kwargs = dict(loc=url, kwargs_xarray={})
m.add_reader(**reader_kwargs)
ds = xr.open_dataset(url, decode_times=False)
m.add_reader(ds=ds)
m.run_all()
```

Expand All @@ -184,7 +184,7 @@ To run an idealized scenario, no reader should be added (`ocean_model` should be
```
from datetime import datetime
m = ptm.OpenDriftModel(lon=4.0, lat=60.0, start_time=datetime(2015, 9, 22, 6),
use_auto_landmask=True,)
use_auto_landmask=True, steps=5)
# idealized simulation, provide a fake current
m.o.set_config('environment:fallback:y_sea_water_velocity', 1)
Expand All @@ -201,7 +201,7 @@ For testing purposes, all steps can be run (including added a "reader") with the
```
from datetime import datetime
m = ptm.OpenDriftModel(lon=4.0, lat=60.0, start_time=datetime(2015, 9, 22, 6),
use_auto_landmask=True, ocean_model="test")
use_auto_landmask=True, ocean_model="test", steps=5)
m.run_all()
```
Expand Down Expand Up @@ -246,7 +246,7 @@ The default list of `export_variables` is set in `config_model` but is modified

#### How to modify details for Stokes Drift

Turn on (on by default):
Turn on (on by default, drift model-dependent):

```
m = ptm.OpenDriftModel(stokes_drift=True)
Expand Down
19 changes: 16 additions & 3 deletions docs/quick_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Run directly from the Lagrangian model you want to use, which will inherit from
```
import particle_tracking_manager as ptm
m = ptm.OpenDriftModel(ocean_model="NWGOA", lon=-151, lat=59)
m = ptm.OpenDriftModel(ocean_model="NWGOA", lon=-151, lat=59, steps=1)
# Can modify `m` between these steps, or look at `OpenDrift` config with `m.drift_model_config()`
m.run_all()
```

Expand All @@ -43,7 +44,13 @@ Then find results in file `m.outfile_name`.
The equivalent for the set up above for using the command line is:

```
ptm lon=-151 lat=59 ocean_model=NWGOA
ptm lon=-151 lat=59 ocean_model=NWGOA steps=1
```

To just initialize the simulation and print the `OpenDrift` configuration to screen without running the simulation, add the `--dry-run` flag:

```
ptm lon=-151 lat=59 ocean_model=NWGOA steps=1 --dry-run
```

`m.outfile_name` is printed to the screen after the command has been run. `ptm` is installed as an entry point with `particle-tracking-manager`.
Expand Down Expand Up @@ -122,11 +129,17 @@ m.reader
Get reader/ocean model properties (gathered metadata about model):

```
m.reader_metadata(key)
m.reader_metadata(<key>)
```

Show configuration details — many more details on this in {doc}`configuration`:

```
m.show_config()
```

Show `OpenDrift` configuration for selected `drift_model`:

```
m.drift_model_config()
```
7 changes: 7 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# What's New

## v0.8.2 (April 10, 2024)

* updated docs
* improved `drift_model_config()`
* updated tests
* now include PTM metadata with output file

## v0.8.1 (April 5, 2024)

* updated docs
Expand Down
29 changes: 21 additions & 8 deletions particle_tracking_manager/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ def main():
help="Input keyword arguments for running PTM. Available options are specific to the `catalog_type`. Dictionary-style input, e.g. `case='Oil'`. Format for list items is e.g. standard_names='[sea_water_practical_salinity,sea_water_temperature]'.",
)

parser.add_argument(
"--dry-run",
help="Return configuration parameters without running the model.",
action=argparse.BooleanOptionalAction,
default=False,
)

args = parser.parse_args()

to_bool = {
Expand All @@ -115,14 +122,20 @@ def main():
}
args.kwargs.update(to_bool)

# # set default
# if "model" not in args:
# args.kwargs["model"] = "opendrift"
m = ptm.OpenDriftModel(**args.kwargs)

# if args.kwargs["ocean_model"] is None and args.kwargs["start_time"] is None:
# raise KeyError("Need to either use a reader or input a start_time to avoid error.")
if args.dry_run:

m = ptm.OpenDriftModel(**args.kwargs)
m.run_all()
# run this to make sure everything is updated fully
m.add_reader()
print(m.drift_model_config())

else:

m.add_reader()
print(m.drift_model_config())

m.seed()
m.run()

print(m.outfile_name)
print(m.outfile_name)
118 changes: 101 additions & 17 deletions particle_tracking_manager/models/opendrift/opendrift.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Using OpenDrift for particle tracking."""
import copy
import datetime
import gc
import json
import logging
import os
import platform
import tempfile

from pathlib import Path
from typing import Optional, Union
Expand Down Expand Up @@ -735,11 +739,17 @@ def run_add_reader(
else:
if ".nc" in loc_remote:
ds = xr.open_dataset(
loc_remote, chunks={}, drop_variables=drop_vars
loc_remote,
chunks={},
drop_variables=drop_vars,
decode_times=False,
)
else:
ds = xr.open_zarr(
loc_remote, chunks={}, drop_variables=drop_vars
loc_remote,
chunks={},
drop_variables=drop_vars,
decode_times=False,
)

# For NWGOA, need to calculate wetdry mask from a variable
Expand Down Expand Up @@ -811,8 +821,12 @@ def run_add_reader(
else:
raise ValueError("reader did not set an ocean_model")

def run_seed(self):
"""Seed drifters for model."""
@property
def seed_kws(self):
"""Gather seed input kwargs.
This could be run more than once.
"""

already_there = [
"seed:number",
Expand All @@ -824,22 +838,28 @@ def run_seed(self):
"seed:droplet_diameter_sigma",
"seed:droplet_diameter_max_subsea",
"seed:object_type",
"seed_flag",
"drift:use_tabularised_stokes_drift",
"drift:vertical_advection",
"drift:truncate_ocean_model_below_m",
]

seed_kws = {
"time": self.start_time.to_pydatetime(),
_seed_kws = {
"time": self.start_time.to_pydatetime()
if self.start_time is not None
else None,
"z": self.z,
}

# update seed_kws with drift_model-specific seed parameters
seedlist = self.drift_model_config(prefix="seed")
seedlist = [(one, two) for one, two in seedlist if one not in already_there]
seedlist = [(one.replace("seed:", ""), two) for one, two in seedlist]
seed_kws.update(seedlist)
_seed_kws.update(seedlist)

if self.seed_flag == "elements":
# add additional seed parameters
seed_kws.update(
_seed_kws.update(
{
"lon": self.lon,
"lat": self.lat,
Expand All @@ -848,20 +868,34 @@ def run_seed(self):
}
)

self.o.seed_elements(**seed_kws)
elif self.seed_flag == "geojson":

# geojson needs string representation of time
_seed_kws["time"] = (
self.start_time.isoformat() if self.start_time is not None else None
)

self._seed_kws = _seed_kws
return self._seed_kws

def run_seed(self):
"""Actually seed drifters for model."""

if self.seed_flag == "elements":

self.o.seed_elements(**self.seed_kws)

elif self.seed_flag == "geojson":

# geojson needs string representation of time
seed_kws["time"] = self.start_time.isoformat()
self.geojson["properties"] = seed_kws
self.seed_kws["time"] = self.start_time.isoformat()
self.geojson["properties"] = self.seed_kws
json_string_dumps = json.dumps(self.geojson)
self.o.seed_from_geojson(json_string_dumps)

else:
raise ValueError(f"seed_flag {self.seed_flag} not recognized.")

self.seed_kws = seed_kws
self.initial_drifters = self.o.elements_scheduled

def run_drifters(self):
Expand All @@ -885,8 +919,8 @@ def run_drifters(self):

self.o._config = config_input_to_opendrift # only OpenDrift config

output_file = (
self.output_file
output_file_initial = (
f"{self.output_file}_initial"
or f"output-results_{datetime.datetime.utcnow():%Y-%m-%dT%H%M:%SZ}.nc"
)

Expand All @@ -895,11 +929,37 @@ def run_drifters(self):
time_step_output=self.time_step_output,
steps=self.steps,
export_variables=self.export_variables,
outfile=output_file,
outfile=output_file_initial,
)

self.o._config = full_config # reinstate config

# open outfile file and add config to it
# config can't be present earlier because it breaks opendrift
ds = xr.open_dataset(output_file_initial)
for k, v in self.drift_model_config():
if isinstance(v, (bool, type(None), pd.Timestamp, pd.Timedelta)):
v = str(v)
ds.attrs[f"ptm_config_{k}"] = v

# Make new output file
output_file = (
self.output_file
or f"output-results_{datetime.datetime.utcnow():%Y-%m-%dT%H%M:%SZ}.nc"
)

ds.to_netcdf(output_file)

# update with new path name
self.o.outfile_name = output_file

try:
# remove initial file to save space
os.remove(output_file_initial)
except PermissionError:
# windows issue
pass

@property
def _config(self):
"""Surface the model configuration."""
Expand Down Expand Up @@ -1011,7 +1071,8 @@ def drift_model_config(self, ptm_level=[1, 2, 3], prefix=""):
This shows all PTM-controlled parameters for the OpenDrift
drift model selected and their current values, at the selected ptm_level
of importance.
of importance. It includes some additional configuration parameters
that are indirectly controlled by PTM parameters.
Parameters
----------
Expand All @@ -1022,14 +1083,37 @@ def drift_model_config(self, ptm_level=[1, 2, 3], prefix=""):
prefix to search config for, only for OpenDrift parameters (not PTM).
"""

return [
outlist = [
(key, value_dict["value"])
for key, value_dict in self.show_config(
substring=":", ptm_level=ptm_level, level=[1, 2, 3], prefix=prefix
).items()
if "value" in value_dict
]

# also PTM config parameters that are separate from OpenDrift parameters
outlist2 = [
(key, value_dict["value"])
for key, value_dict in self.show_config(
ptm_level=ptm_level, prefix=prefix
).items()
if "od_mapping" not in value_dict and "value" in value_dict
]

# extra parameters that are not in the config_model but are set by PTM indirectly
extra_keys = [
"drift:vertical_advection",
"drift:truncate_ocean_model_below_m",
"drift:use_tabularised_stokes_drift",
]
outlist += [
(key, self.show_config(key=key)["value"])
for key in extra_keys
if "value" in self.show_config(key=key)
]

return outlist + outlist2

def get_configspec(self, prefix, substring, excludestring, level, ptm_level):
"""Copied from OpenDrift, then modified."""

Expand Down
Loading

0 comments on commit 1f8ffb7

Please sign in to comment.