diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 31ca5c6..ebef840 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,21 +28,38 @@ jobs: with: path: ~/conda_pkgs_dir key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('ci/environment-py${{ matrix.python-version }}.yml') }} - - uses: conda-incubator/setup-miniconda@v3 + # - uses: conda-incubator/setup-miniconda@v3 + # with: + # # mamba-version: "*" # activate this to build with mamba. + # python-version: ${{ matrix.python-version }} + # miniforge-variant: Mambaforge + # channels: conda-forge, defaults # These need to be specified to use mamba + # channel-priority: true + # environment-file: ci/environment-py${{ matrix.python-version }}.yml + + # activate-environment: test_env_particle-tracking-manager + # use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly! + # - name: Set up conda environment + # shell: bash -l {0} + # run: | + # python -m pip install -e . --no-deps --force-reinstall + + - name: Setup Micromamba Python ${{ matrix.python-version }} + uses: mamba-org/setup-micromamba@v1 with: - # mamba-version: "*" # activate this to build with mamba. - python-version: ${{ matrix.python-version }} - miniforge-variant: Mambaforge - channels: conda-forge, defaults # These need to be specified to use mamba - channel-priority: true + init-shell: bash + create-args: >- + python=${{ matrix.python-version }} --channel conda-forge environment-file: ci/environment-py${{ matrix.python-version }}.yml + cache-environment: true + post-cleanup: 'all' - activate-environment: test_env_particle-tracking-manager - use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly! - - name: Set up conda environment + - name: Install package shell: bash -l {0} run: | python -m pip install -e . --no-deps --force-reinstall + + - name: Run Tests shell: bash -l {0} run: | diff --git a/ci/environment-py3.10.yml b/ci/environment-py3.10.yml index a74fe21..1ca4c53 100644 --- a/ci/environment-py3.10.yml +++ b/ci/environment-py3.10.yml @@ -4,42 +4,43 @@ channels: dependencies: - python=3.10 ############## These will have to be adjusted to your specific project - # - adios_db + # - adios-db - appdirs - aiohttp + - kerchunk - numpy - # opendrift reqs - - matplotlib>=3.5 - - numpy>=1.17 - - scipy>=1.6 - - netcdf4<=1.6.1 - - ffmpeg - - pyproj>=2.3 - - libgdal>=3.1 - - gdal>=3.1 - - xarray - - dask - - cfgrib - - pygrib - - xhistogram - - requests - - pytest<8 - - pytest-cov - - pytest-benchmark - - pytest-mpl - - cartopy>=0.20 - - nc-time-axis - - geojson - - pynucos>=2.12 - - isodate - - coloredlogs - - cmocean - - utm - - roaring-landmask>=0.7 - # - trajan>=0.1.3 - # - adios_db + # # opendrift reqs + # - matplotlib>=3.5 + # - numpy>=1.17 + # - scipy>=1.6 + # - netcdf4<=1.6.1 + # - ffmpeg + # - pyproj>=2.3 + # - libgdal>=3.1 + # - gdal>=3.1 + # - xarray + # - dask + # - cfgrib + # - pygrib + # - xhistogram + # - requests + # - pytest<8 + # - pytest-cov + # - pytest-benchmark + # - pytest-mpl + # - cartopy>=0.20 + # - nc-time-axis + # - geojson + # - pynucos>=2.12 + # - isodate + # - coloredlogs + # - cmocean + # - utm + # - roaring-landmask>=0.7 + # # - trajan>=0.1.3 + # # - adios_db ## - #- opendrift + - opendrift>=1.11.2 - scipy - xarray # - xroms @@ -49,7 +50,8 @@ dependencies: - coverage - pip - pytest - - pip: - - git+https://github.com/OpenDrift/opendrift - - git+https://github.com/fsspec/kerchunk + # - pip: + # - adios_db + # - git+https://github.com/OpenDrift/opendrift + # - git+https://github.com/fsspec/kerchunk # - xroms # can't be found on conda-forge for CI, but don't need since in runslow tests? diff --git a/ci/environment-py3.11.yml b/ci/environment-py3.11.yml index e25b528..3753604 100644 --- a/ci/environment-py3.11.yml +++ b/ci/environment-py3.11.yml @@ -4,42 +4,43 @@ channels: dependencies: - python=3.11 ############## These will have to be adjusted to your specific project - # - adios_db + # - adios-db - appdirs - aiohttp + - kerchunk - numpy - # opendrift reqs - - matplotlib>=3.5 - - numpy>=1.17 - - scipy>=1.6 - - netcdf4<=1.6.1 - - ffmpeg - - pyproj>=2.3 - - libgdal>=3.1 - - gdal>=3.1 - - xarray - - dask - - cfgrib - - pygrib - - xhistogram - - requests - - pytest<8 - - pytest-cov - - pytest-benchmark - - pytest-mpl - - cartopy>=0.20 - - nc-time-axis - - geojson - - pynucos>=2.12 - - isodate - - coloredlogs - - cmocean - - utm - - roaring-landmask>=0.7 + # # opendrift reqs + # - matplotlib>=3.5 + # - numpy>=1.17 + # - scipy>=1.6 + # - netcdf4<=1.6.1 + # - ffmpeg + # - pyproj>=2.3 + # - libgdal>=3.1 + # - gdal>=3.1 + # - xarray + # - dask + # - cfgrib + # - pygrib + # - xhistogram + # - requests + # - pytest<8 + # - pytest-cov + # - pytest-benchmark + # - pytest-mpl + # - cartopy>=0.20 + # - nc-time-axis + # - geojson + # - pynucos>=2.12 + # - isodate + # - coloredlogs + # - cmocean + # - utm + # - roaring-landmask>=0.7 # - trajan>=0.1.3 # - adios_db ## - #- opendrift + - opendrift>=1.11.2 - scipy - xarray # - xroms @@ -49,7 +50,8 @@ dependencies: - coverage - pip - pytest - - pip: - - git+https://github.com/OpenDrift/opendrift - - git+https://github.com/fsspec/kerchunk + # - pip: + # - adios_db + # - git+https://github.com/OpenDrift/opendrift + # - git+https://github.com/fsspec/kerchunk # - xroms # can't be found on conda-forge for CI, but don't need since in runslow tests? diff --git a/ci/environment-py3.9.yml b/ci/environment-py3.9.yml index 3a3f12b..6d2c4c5 100644 --- a/ci/environment-py3.9.yml +++ b/ci/environment-py3.9.yml @@ -4,42 +4,43 @@ channels: dependencies: - python=3.9 ############## These will have to be adjusted to your specific project - # - adios_db + # - adios-db - appdirs - aiohttp + - kerchunk - numpy - # opendrift reqs - - matplotlib>=3.5 - - numpy>=1.17 - - scipy>=1.6 - - netcdf4<=1.6.1 - - ffmpeg - - pyproj>=2.3 - - libgdal>=3.1 - - gdal>=3.1 - - xarray - - dask - - cfgrib - - pygrib - - xhistogram - - requests - - pytest<8 - - pytest-cov - - pytest-benchmark - - pytest-mpl - - cartopy>=0.20 - - nc-time-axis - - geojson - - pynucos>=2.12 - - isodate - - coloredlogs - - cmocean - - utm - - roaring-landmask>=0.7 + # # opendrift reqs + # - matplotlib>=3.5 + # - numpy>=1.17 + # - scipy>=1.6 + # - netcdf4<=1.6.1 + # - ffmpeg + # - pyproj>=2.3 + # - libgdal>=3.1 + # - gdal>=3.1 + # - xarray + # - dask + # - cfgrib + # - pygrib + # - xhistogram + # - requests + # - pytest<8 + # - pytest-cov + # - pytest-benchmark + # - pytest-mpl + # - cartopy>=0.20 + # - nc-time-axis + # - geojson + # - pynucos>=2.12 + # - isodate + # - coloredlogs + # - cmocean + # - utm + # - roaring-landmask>=0.7 # - trajan>=0.1.3 # - adios_db ## - #- opendrift + - opendrift>=1.11.2 - scipy - xarray # - xroms @@ -49,7 +50,8 @@ dependencies: - coverage - pip - pytest - - pip: - - git+https://github.com/OpenDrift/opendrift - - git+https://github.com/fsspec/kerchunk + # - pip: + # - adios_db + # - git+https://github.com/OpenDrift/opendrift + # - git+https://github.com/fsspec/kerchunk # - xroms # can't be found on conda-forge for CI, but don't need since in runslow tests? diff --git a/docs/api.rst b/docs/api.rst index 88529be..708cdbd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,4 +9,4 @@ API :recursive: the_manager - models.opendrift + models diff --git a/docs/configuration.md b/docs/configuration.md index f5087a8..b67e175 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -160,9 +160,11 @@ m.show_config(key="ocean_model") The built-in ocean models are: * 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.) * 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.) -* CIOFS_NOW (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, +* 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, 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.) +If you are running locally on an Axiom server you can use `ocean_model_local=True` to access the model output locally instead of remotely. + An alternative ocean model can be used instead by initializing the `Manager` then setting up the reader manually, as shown in a {ref}`Quick Start` example: ``` @@ -239,6 +241,8 @@ To limit the variables saved in the export file, input a list of just the variab m = ptm.OpenDriftModel(export_variables=[]) ``` +The default list of `export_variables` is set in `config_model` but is modified depending on the `drift_model` set. + #### How to modify details for Stokes Drift diff --git a/docs/environment.yml b/docs/environment.yml index d93874c..bc68348 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -8,6 +8,7 @@ dependencies: - adios_db - aiohttp - appdirs + - cmocean - numpy - xarray # These are needed for the docs themselves diff --git a/docs/index.rst b/docs/index.rst index 8e54638..ef7bf80 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ To install from PyPI: :caption: Examples and demos quick_start + tutorial configuration diff --git a/docs/quick_start.md b/docs/quick_start.md index 529b41d..b927efe 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -59,19 +59,28 @@ This demo will run using easily-available ROMS model output from `xroms`. import particle_tracking_manager as ptm import xroms +import xarray as xr -m = ptm.OpenDriftModel(lon = -90, lat = 28.7, number=1, steps=2) +m = ptm.OpenDriftModel(lon = -90, lat = 28.7, number=10, steps=20, + use_static_masks=True) + 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() or the following m.seed() m.run() ``` +Plot using `OpenDrift`'s built in plotting. Many options are available, including animations (see [OpenDrift docs for more information](https://opendrift.github.io/)). + +```{code-cell} ipython3 +m.o.plot(fast=True) +``` + ## Idealized simulation To run an idealized scenario, no reader should be added but configuration parameters can be manually changed, for example: @@ -80,7 +89,7 @@ To run an idealized scenario, no reader should be added but configuration parame import particle_tracking_manager as ptm 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=20) # idealized simulation, provide a fake current m.o.set_config('environment:fallback:y_sea_water_velocity', 1) @@ -92,6 +101,10 @@ m.seed() m.run() ``` +```{code-cell} ipython3 +m.o.plot(fast=True) +``` + ## Ways to Get Information Check drifter initialization properties: diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..8bedd51 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,291 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.1 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# Tutorial + +Particle Tracking Manager (PTM) is a wrapper around particle tracking codes to easily run particle simulations in select (or user-input) ocean models. Currently, `OpenDrift` is included. In this tutorial we demonstrate using the four wrapped drift models from `OpenDrift` along with some high level configuration changes. + +```{code-cell} ipython3 +import xarray as xr +import particle_tracking_manager as ptm +import xroms +import cmocean.cm as cmo +``` + +## Ocean Models + +### Known Models + +Three ocean models are built into PTM and can be accessed by name `ocean_model=` "NWGOA", "CIOFS", and "CIOFSOP", and either accessed remotely or locally if run on an internal server (at Axiom) (with `ocean_model_local=True`). + +### Wet/dry vs. Static Masks + +The known models in PTM have wet/dry masks from ROMS so they have had to be specially handled, requiring some new development in `OpenDrift`. There are two options: + +* (DEFAULT) Use the typical, static, ROMS masks (`mask_rho`, `mask_u`, `mask_v`). For ROMS simulations run in [wet/dry mode](https://www.myroms.org/wiki/WET_DRY), grid cells in `mask_rho` are 0 if they are permanently dry and 1 if they are ever wet. This saves some computational time but is inconsistent with the ROMS output files in some places since the drifters may be allowed (due to the static mask) to enter a cell they wouldn't otherwise. However, it doesn't make much of a difference for simulations that aren't in the tidal flats. +* Use the time-varying wet/dry masks (`wetdry_mask_rho`, `wetdry_mask_u`, `wetdry_mask_v`). This costs some more computational time but is fully consistent with the ROMS output files. This option should be selected if drifters are expected to run in the tidal flats. + +### User-input Models + +As opposed to known models, a user can input their own xarray Dataset, which we will do for this tutorial. When you input your own Dataset, you have to add the reader by hand as opposed to being able to input the `ocean_model` name in the initial call. + +```{code-cell} ipython3 +url = xroms.datasets.CLOVER.fetch("ROMS_example_full_grid.nc") +ds = xr.open_dataset(url, decode_times=False) +``` + +## Drift Models + +After a drift simulation is run, results can be found in file with name `m.outfile_name`. + +### OceanDrift (Physics) + +This model can in 2D or 3D with or without horizontal or vertical mixing, wind drift, Stokes drift, etc. By default this would be run at the surface in 2D but we can input selections to run in 3D starting at depth. + +#### Initialize manager `m` + +```{code-cell} ipython3 +m = ptm.OpenDriftModel(lon=-90, lat=28.7, number=10, steps=40, + z=-5, do3D=True, horizontal_diffusivity=100,) +``` + +The drift_model-specific parameters chosen by the user and PTM for this simulation are: + +```{code-cell} ipython3 +m.drift_model_config() +``` + +You can also find the full PTM and `OpenDrift` configuration information with: + +```{code-cell} ipython3 +--- +editable: true +slideshow: + slide_type: '' +--- +m.show_config() +``` + +#### Add reader and run + +```{code-cell} ipython3 +m.add_reader(ds=ds) +m.run_all() +``` + +#### Plot + +```{code-cell} ipython3 +m.o.plot(linecolor="z", fast=True, cmap=cmo.deep) +``` + +### Leeway (Search and Rescue) + +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. + +#### Initialize manager `m` + +```{code-cell} ipython3 +m = ptm.OpenDriftModel(drift_model="Leeway", lon = -89.8, lat = 29.08, + number=10, steps=40, + object_type="Fishing vessel, general (mean values)") + +# This drift model requires wind data to be set which isn't present in model output +m.o.set_config('environment:constant:x_wind', -1) +m.o.set_config('environment:constant:y_wind', 1) +``` + +The objects that can be modeled are: + +```{code-cell} ipython3 +m.show_config(key="seed:object_type")["enum"] +``` + +The drift_model-specific parameters chosen by the user and PTM for this simulation are: + +```{code-cell} ipython3 +m.drift_model_config() +``` + +#### Add reader and run + +```{code-cell} ipython3 +m.add_reader(ds=ds) +m.run_all() +``` + +#### Plot + +```{code-cell} ipython3 +m.o.plot(fast=True) +``` + +### LarvalFish + +This model simulates eggs and larvae that move in 3D with the currents and some basic behavior and vertical movement. It also simulates some basic growth of the larvae. + +There are specific seeding options for this model: +* 'diameter' +* 'neutral_buoyancy_salinity' +* 'stage_fraction' +* 'hatched' +* 'length' +* 'weight' + +#### Eggs + +An optional general flag is to initialize the drifters at the seabed, which might make sense for eggs and is demonstrated here. + +##### Initialize manager `m` + +```{code-cell} ipython3 +m = ptm.OpenDriftModel(drift_model="LarvalFish", lon=-89.85, lat=28.8, number=10, steps=45, + do3D=True, seed_seafloor=True) +``` + +The drift_model-specific parameters chosen by the user and PTM for this simulation are: + +```{code-cell} ipython3 +m.drift_model_config() +``` + +##### Add reader and run + +```{code-cell} ipython3 +m.add_reader(ds=ds) +m.run_all() +``` + +##### Plot + +```{code-cell} ipython3 +m.o.plot(linecolor="z", fast=True, cmap=cmo.deep_r) +``` + +```{code-cell} ipython3 +m.o.plot_property('length') +m.o.plot_property('weight') +m.o.plot_property('diameter') +m.o.plot_property('stage_fraction') +``` + +Output from the simulation can be viewed in the history or elements, or from the output file. + +```{code-cell} ipython3 +m.outfile_name +``` + +```{code-cell} ipython3 +m.o.history["z"].data +``` + +```{code-cell} ipython3 +m.o.elements +``` + +#### Hatched! + +##### Initialize manager `m` + +```{code-cell} ipython3 +m = ptm.OpenDriftModel(drift_model="LarvalFish", lon=-89.85, lat=28.8, number=10, steps=45, + do3D=True, seed_seafloor=True, hatched=1) +``` + +The drift_model-specific parameters chosen by the user and PTM for this simulation are: + +```{code-cell} ipython3 +m.drift_model_config() +``` + +##### Add reader and run + +```{code-cell} ipython3 +m.add_reader(ds=ds) +m.run_all() +``` + +##### Plot + +```{code-cell} ipython3 +m.o.plot(linecolor="z", fast=True, cmap=cmo.deep_r) +``` + +```{code-cell} ipython3 +m.o.plot_property('length') +m.o.plot_property('weight') +m.o.plot_property('diameter') +m.o.plot_property('stage_fraction') +``` + +### OpenOil + +This model simulates the transport of oil. Processes optionally modeled (which are included in PTM by default) include: +* "emulsification" +* "dispersion" +* "evaporation" +* "update_oilfilm_thickness" +* "biodegradation" + +There are also specific seeding options for this model: +* "m3_per_hour" +* "oil_film_thickness" +* "droplet_size_distribution" +* "droplet_diameter_mu" +* "droplet_diameter_sigma" +* "droplet_diameter_min_subsea" +* "droplet_diameter_max_subsea" + +#### Initialize manager `m` + +```{code-cell} ipython3 +m = ptm.OpenDriftModel(drift_model="OpenOil", lon=-89.85, lat=28.08, number=10, steps=45, + z=-10, do3D=True, oil_type='GENERIC BUNKER C') +m.o.set_config('environment:constant:x_wind', -1) +m.o.set_config('environment:constant:y_wind', 1) +``` + +List available oil types from NOAA's ADIOS database: + +```{code-cell} ipython3 +m.show_config(key="seed:oil_type") +``` + +The drift_model-specific parameters chosen by the user and PTM for this simulation are: + +```{code-cell} ipython3 +m.drift_model_config() +``` + +#### Add reader and run + +```{code-cell} ipython3 +m.add_reader(ds=ds) +m.run_all() +``` + +#### Plot + +```{code-cell} ipython3 +m.o.plot(linecolor="z", fast=True, cmap=cmo.deep_r) +``` + +Plot the oil budget. + +```{code-cell} ipython3 +m.o.plot_oil_budget(show_wind_and_current=False) +``` + +```{code-cell} ipython3 + +``` diff --git a/docs/whats_new.md b/docs/whats_new.md index 2fd88da..4c81995 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1,5 +1,9 @@ # What's New +## v0.8.1 (April 5, 2024) + +* updated docs + ## v0.8.0 (April 2, 2024) * `time_step_output` behavior has changed — 1 hour by default diff --git a/environment.yml b/environment.yml index 7452fc1..5bd95a2 100644 --- a/environment.yml +++ b/environment.yml @@ -11,6 +11,7 @@ dependencies: - fastparquet - jupyter - jupyterlab + - kerchunk - xarray - numpy - opendrift diff --git a/particle_tracking_manager/models/opendrift/opendrift.py b/particle_tracking_manager/models/opendrift/opendrift.py index 0dc10bf..e77cd3c 100644 --- a/particle_tracking_manager/models/opendrift/opendrift.py +++ b/particle_tracking_manager/models/opendrift/opendrift.py @@ -405,10 +405,28 @@ def __setattr_model__(self, name: str, value) -> None: self.o.set_config("drift:vertical_advection", True) elif name == "do3D" and value and self.drift_model == "Leeway": self.logger.info( - "do3D is True but drift_model is Leeway so " "changing do3D to False." + "do3D is True but drift_model is Leeway so changing do3D to False." ) self.do3D = False + # if drift_model is LarvalFish, vertical_mixing has to be True + if name == "vertical_mixing" and not value and self.drift_model == "LarvalFish": + raise ValueError( + "drift_model is LarvalFish which always has vertical mixing on in OpenDrift so vertical_mixing must be True." + ) + + # if drift_model is LarvalFish, surface_only can't be True + if name == "surface_only" and value and self.drift_model == "LarvalFish": + raise ValueError( + "drift_model is LarvalFish which is always 3D in OpenDrift so surface_only must be False." + ) + + # if drift_model is LarvalFish, do3D has to be True + if name == "do3D" and not value and self.drift_model == "LarvalFish": + raise ValueError( + "drift_model is LarvalFish which is always 3D in OpenDrift so do3D must be True." + ) + # Make sure vertical_mixing_timestep equals default value if vertical_mixing False if name in ["vertical_mixing", "vertical_mixing_timestep"]: vmtdef = self.config_model["vertical_mixing_timestep"]["default"] @@ -506,6 +524,17 @@ def __setattr_model__(self, name: str, value) -> None: vars = ["object_type"] self.__dict__["export_variables"] += vars self.config_model["export_variables"]["value"] += vars + elif name == "export_variables" and self.drift_model == "LarvalFish": + vars = [ + "diameter", + "neutral_buoyancy_salinity", + "stage_fraction", + "hatched", + "length", + "weight", + ] + self.__dict__["export_variables"] += vars + self.config_model["export_variables"]["value"] += vars self._update_config() @@ -725,13 +754,12 @@ def run_add_reader( # ds["angle"] = grid["angle"] try: - units_date = pd.Timestamp( - ds.ocean_time.attrs["units"].split("since ")[1] - ) - except KeyError: # for remote - units_date = pd.Timestamp( - ds.ocean_time.encoding["units"].split("since ")[0] - ) + units = ds.ocean_time.attrs["units"] + except KeyError: + units = ds.ocean_time.encoding["units"] + datestr = units.split("since ")[1] + units_date = pd.Timestamp(datestr) + # use reader start time if not otherwise input if self.start_time is None: self.logger.info("setting reader start_time as simulation start_time") @@ -740,11 +768,14 @@ def run_add_reader( self.start_time = units_date + pd.to_timedelta( ds.ocean_time[0].values, unit="s" ) - # narrow model output to simulation time if possible before sending to Reader if self.start_time is not None and self.end_time is not None: + dt_model = float( + ds.ocean_time[1] - ds.ocean_time[0] + ) # time step of the model output in seconds start_time_num = (self.start_time - units_date).total_seconds() - end_time_num = (self.end_time - units_date).total_seconds() + # want to include the next ocean model output after the last drifter simulation time + end_time_num = (self.end_time - units_date).total_seconds() + dt_model ds = ds.sel(ocean_time=slice(start_time_num, end_time_num)) self.logger.info("Narrowed model output to simulation time") else: @@ -783,11 +814,29 @@ def run_add_reader( def run_seed(self): """Seed drifters for model.""" + already_there = [ + "seed:number", + "seed:z", + "seed:seafloor", + "seed:droplet_diameter_mu", + "seed:droplet_diameter_min_subsea", + "seed:droplet_size_distribution", + "seed:droplet_diameter_sigma", + "seed:droplet_diameter_max_subsea", + "seed:object_type", + ] + seed_kws = { "time": self.start_time.to_pydatetime(), "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) + if self.seed_flag == "elements": # add additional seed parameters seed_kws.update( diff --git a/particle_tracking_manager/the_manager.py b/particle_tracking_manager/the_manager.py index ba56126..cc8c5f8 100644 --- a/particle_tracking_manager/the_manager.py +++ b/particle_tracking_manager/the_manager.py @@ -326,7 +326,7 @@ def __setattr__(self, name: str, value) -> None: # deal with if input longitudes need to be shifted due to model if name == "oceanmodel_lon0_360" and value: - if self.ocean_model is not "test" and self.lon is not None: + if self.ocean_model != "test" and self.lon is not None: # move longitude to be 0 to 360 for this model # this is not a user-defined option if -180 < self.lon < 0: diff --git a/particle_tracking_manager/the_manager_config.json b/particle_tracking_manager/the_manager_config.json index d58c4db..38013a9 100644 --- a/particle_tracking_manager/the_manager_config.json +++ b/particle_tracking_manager/the_manager_config.json @@ -159,7 +159,7 @@ }, "use_static_masks": { "type": "bool", - "default": false, + "default": true, "ptm_level": 2, "description": "Set to True to use static masks for known models instead of wetdry masks. If False, the masks are change in time." }, diff --git a/tests/test_opendrift.py b/tests/test_opendrift.py index 43854f5..1fd212e 100644 --- a/tests/test_opendrift.py +++ b/tests/test_opendrift.py @@ -119,16 +119,9 @@ def test_init(self): ) -class TestOpenDriftModel_OceanDrift(unittest.TestCase): +class TestOpenDriftModel_OceanDrift_static_mask(unittest.TestCase): def setUp(self): - self.model = OpenDriftModel(drift_model="OceanDrift") - - def test_error_no_end_of_simulation(self): - self.model.do3D = False - self.use_static_masks = False - # need to input steps, duration, or end_time but don't here - with pytest.raises(ValueError): - self.model.add_reader(ds=ds) + self.model = OpenDriftModel(drift_model="OceanDrift", use_static_masks=True) def test_ocean_model_not_known_ds_None(self): self.model.ocean_model = "wrong_name" @@ -137,23 +130,6 @@ def test_ocean_model_not_known_ds_None(self): with pytest.raises(ValueError): self.model.add_reader(ds=ds) - def test_drop_vars_do3D_false(self): - self.model.do3D = False - self.use_static_masks = False - self.model.steps = 4 - self.model.add_reader(ds=ds) - assert self.model.reader.variables == [ - "x_sea_water_velocity", - "y_sea_water_velocity", - "land_binary_mask", - "x_wind", - "y_wind", - "wind_speed", - "sea_water_speed", - ] - assert "wetdry_mask_rho" in self.model.reader.Dataset.data_vars - assert "mask_rho" not in self.model.reader.Dataset.data_vars - def test_drop_vars_do3D_true(self): self.model.do3D = True self.model.steps = 4 @@ -171,7 +147,6 @@ def test_drop_vars_do3D_true(self): def test_drop_vars_use_static_masks(self): self.model.do3D = False - self.model.use_static_masks = True self.model.duration = pd.Timedelta("24h") self.model.add_reader(ds=ds) assert self.model.reader.variables == [ @@ -201,28 +176,38 @@ def test_drop_vars_no_wind(self): ] -class TestOpenDriftModel_LarvalFish(unittest.TestCase): +class TestOpenDriftModel_OceanDrift_wetdry_mask(unittest.TestCase): def setUp(self): - self.model = OpenDriftModel(drift_model="LarvalFish") + self.model = OpenDriftModel(drift_model="OceanDrift", use_static_masks=False) + + def test_error_no_end_of_simulation(self): + self.model.do3D = False + # need to input steps, duration, or end_time but don't here + with pytest.raises(ValueError): + self.model.add_reader(ds=ds) def test_drop_vars_do3D_false(self): self.model.do3D = False - self.model.end_time = pd.Timestamp("1970-01-01T02:00") + self.model.steps = 4 self.model.add_reader(ds=ds) assert self.model.reader.variables == [ "x_sea_water_velocity", "y_sea_water_velocity", - "sea_water_salinity", - "sea_water_temperature", "land_binary_mask", "x_wind", "y_wind", "wind_speed", "sea_water_speed", ] + assert "wetdry_mask_rho" in self.model.reader.Dataset.data_vars + assert "mask_rho" not in self.model.reader.Dataset.data_vars - def test_drop_vars_do3D_true(self): - self.model.do3D = True + +class TestOpenDriftModel_LarvalFish(unittest.TestCase): + def setUp(self): + self.model = OpenDriftModel(drift_model="LarvalFish", do3D=True) + + def test_drop_vars_wind(self): self.model.duration = pd.Timedelta("1h") self.model.add_reader(ds=ds) assert self.model.reader.variables == [ @@ -238,26 +223,8 @@ def test_drop_vars_do3D_true(self): "sea_water_speed", ] - def test_drop_vars_no_wind(self): - self.model.stokes_drift = False - self.model.wind_drift_factor = 0 - self.model.wind_uncertainty = 0 - self.model.vertical_mixing = False - self.model.steps = 3 - self.model.add_reader(ds=ds) - # import pdb; pdb.set_trace() - assert self.model.reader.variables == [ - "x_sea_water_velocity", - "y_sea_water_velocity", - "sea_water_salinity", - "sea_water_temperature", - "land_binary_mask", - "sea_water_speed", - ] - def test_drift_model(): - """can't change the drift_model after class initialization""" with pytest.raises(ValueError): m = OpenDriftModel(drift_model="not_a_real_model") @@ -268,6 +235,7 @@ def setUp(self): # self.m.config_model = {"test_key": {"value": "old_value"}} def test_set_drift_model(self): + """can't change the drift_model after class initialization""" with self.assertRaises(KeyError): self.m.drift_model = "new_model" @@ -326,12 +294,16 @@ def test_vertical_mixing_false_mixed_layer_depth_not_default(self): class TestOpenDriftModel_Leeway(unittest.TestCase): def setUp(self): - self.m = OpenDriftModel(drift_model="Leeway") + self.m = OpenDriftModel( + drift_model="Leeway", object_type=">PIW, scuba suit (face up)" + ) def test_leeway_model_wind_drift_factor_not_default(self): self.m.wind_drift_factor = 10 d = self.m.show_config(key="wind_drift_factor") assert d["value"] == d["default"] + assert self.m.object_type == ">PIW, scuba suit (face up)" + assert self.m.o._config["object_type"]["value"] == ">PIW, scuba suit (face up)" def test_leeway_model_wind_drift_depth_not_default(self): self.m.wind_drift_depth = 10 @@ -364,5 +336,96 @@ def test_horizontal_diffusivity_logic(): assert m.horizontal_diffusivity == 150.0 # known grid values +def test_LarvalFish_disallowed_settings(): + """LarvalFish is incompatible with some settings. + + LarvalFish has to always be 3D. + """ + + with pytest.raises(ValueError): + m = OpenDriftModel(drift_model="LarvalFish", vertical_mixing=False) + + with pytest.raises(ValueError): + m = OpenDriftModel(drift_model="LarvalFish", surface_only=True) + + m = OpenDriftModel(drift_model="LarvalFish", do3D=True) + with pytest.raises(ValueError): + m.surface_only = True + + with pytest.raises(ValueError): + m = OpenDriftModel(drift_model="LarvalFish", do3D=False) + + +def test_LarvalFish_seeding(): + """Make sure special seed parameter comes through""" + + m = OpenDriftModel( + drift_model="LarvalFish", + lon=-151, + lat=60, + do3D=True, + hatched=1, + start_time="2021-01-01T00:00:00", + use_auto_landmask=True, + ) + m.seed() + assert m.o.elements_scheduled.hatched == 1 + + +def test_OpenOil_seeding(): + """Make sure special seed parameters comes through""" + + m = OpenDriftModel( + drift_model="OpenOil", + lon=-151, + lat=60, + do3D=True, + start_time="2021-01-01T00:00:00", + use_auto_landmask=True, + m3_per_hour=5, + droplet_diameter_max_subsea=0.1, + droplet_diameter_min_subsea=0.01, + droplet_diameter_mu=0.01, + droplet_size_distribution="normal", + droplet_diameter_sigma=10, + oil_film_thickness=5, + oil_type="GENERIC DIESEL", + ) + m.o.set_config("environment:constant:x_wind", -1) + m.o.set_config("environment:constant:y_wind", -1) + m.o.set_config("environment:constant:x_sea_water_velocity", -1) + m.o.set_config("environment:constant:y_sea_water_velocity", -1) + m.seed() + + # to check impact of m3_per_hour: mass_oil for m3_per_hour of 1 * 5 + assert np.allclose(m.o.elements_scheduled.mass_oil, 8.412 * 5) + assert m.o._config["m3_per_hour"]["value"] == 5 + assert m.o._config["droplet_diameter_max_subsea"]["value"] == 0.1 + assert m.o._config["droplet_diameter_min_subsea"]["value"] == 0.01 + assert m.o._config["droplet_diameter_mu"]["value"] == 0.01 + assert m.o._config["droplet_size_distribution"]["value"] == "normal" + assert m.o._config["droplet_diameter_sigma"]["value"] == 10 + assert m.o.elements_scheduled.oil_film_thickness == 5 + assert m.o._config["oil_type"]["value"] == "GENERIC DIESEL" + + +def test_wind_drift(): + """Make sure changed wind drift numbers comes through""" + + m = OpenDriftModel( + drift_model="OceanDrift", + lon=-151, + lat=60, + do3D=True, + wind_drift_factor=1, + wind_drift_depth=10, + start_time="2021-01-01T00:00:00", + use_auto_landmask=True, + ) + m.seed() + assert m.o.elements_scheduled.wind_drift_factor == 1 + assert m.o._config["wind_drift_depth"]["value"] == 10 + + if __name__ == "__main__": unittest.main()