Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Other instrument tests #37

Merged
merged 16 commits into from
Jun 24, 2024
108 changes: 97 additions & 11 deletions tests/instruments/test_adcp.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,119 @@
"""Test the simulation of ADCP instruments."""

import datetime

import numpy as np
import py
import xarray as xr
from parcels import FieldSet

from virtual_ship import Location, Spacetime
from virtual_ship.instruments.adcp import simulate_adcp


def test_simulate_adcp() -> None:
MAX_DEPTH = -1000
MIN_DEPTH = -5
BIN_SIZE = 24
def test_simulate_adcp(tmpdir: py.path.LocalPath) -> None:
# maximum depth the ADCP can measure
MAX_DEPTH = -1000 # -1000
# minimum depth the ADCP can measure
MIN_DEPTH = -5 # -5
# How many samples to take in the complete range between max_depth and min_depth.
NUM_BINS = 40

# arbitrary time offset for the dummy fieldset
base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d")

# where to sample
sample_points = [
Spacetime(Location(1, 2), base_time + datetime.timedelta(seconds=0)),
Spacetime(Location(3, 4), base_time + datetime.timedelta(seconds=1)),
]

# expected observations at sample points
expected_obs = [
{
"U": {"surface": 5, "max_depth": 6},
"V": {"surface": 7, "max_depth": 8},
"lon": sample_points[0].location.lon,
"lat": sample_points[0].location.lat,
"time": base_time + datetime.timedelta(seconds=0),
},
{
"U": {"surface": 9, "max_depth": 10},
"V": {"surface": 11, "max_depth": 12},
"lon": sample_points[1].location.lon,
"lat": sample_points[1].location.lat,
"time": base_time + datetime.timedelta(seconds=1),
},
]

# create fieldset based on the expected observations
# indices are time, depth, latitude, longitude
u = np.zeros((2, 2, 2, 2))
u[0, 0, 0, 0] = expected_obs[0]["U"]["max_depth"]
u[0, 1, 0, 0] = expected_obs[0]["U"]["surface"]
u[1, 0, 1, 1] = expected_obs[1]["U"]["max_depth"]
u[1, 1, 1, 1] = expected_obs[1]["U"]["surface"]

v = np.zeros((2, 2, 2, 2))
v[0, 0, 0, 0] = expected_obs[0]["V"]["max_depth"]
v[0, 1, 0, 0] = expected_obs[0]["V"]["surface"]
v[1, 0, 1, 1] = expected_obs[1]["V"]["max_depth"]
v[1, 1, 1, 1] = expected_obs[1]["V"]["surface"]

fieldset = FieldSet.from_data(
{"U": 0, "V": 0},
{
"lon": 0,
"lat": 0,
"time": [np.datetime64("1950-01-01") + np.timedelta64(632160, "h")],
"U": u,
"V": v,
},
{
"lon": np.array([expected_obs[0]["lon"], expected_obs[1]["lon"]]),
"lat": np.array([expected_obs[0]["lat"], expected_obs[1]["lat"]]),
"depth": np.array([MAX_DEPTH, MIN_DEPTH]),
"time": np.array(
[
np.datetime64(expected_obs[0]["time"]),
np.datetime64(expected_obs[1]["time"]),
]
),
},
)

sample_points = [Spacetime(Location(0, 0), 0)]
# perform simulation
out_path = tmpdir.join("out.zarr")

simulate_adcp(
fieldset=fieldset,
out_file_name="test",
out_path=out_path,
max_depth=MAX_DEPTH,
min_depth=MIN_DEPTH,
bin_size=BIN_SIZE,
num_bins=NUM_BINS,
sample_points=sample_points,
)

results = xr.open_zarr(out_path)

# test if output is as expected
assert len(results.trajectory) == NUM_BINS

# for every obs, check if the variables match the expected observations
# we only verify at the surface and max depth of the adcp, because in between is tricky
for traj, vert_loc in [
(results.trajectory[0], "max_depth"),
(results.trajectory[-1], "surface"),
]:
obs_all = results.sel(trajectory=traj).obs
assert len(obs_all) == len(sample_points)
for i, (obs_i, exp) in enumerate(zip(obs_all, expected_obs, strict=True)):
obs = results.sel(trajectory=traj, obs=obs_i)
for var in ["lat", "lon"]:
obs_value = obs[var].values.item()
exp_value = exp[var]
assert np.isclose(
obs_value, exp_value
), f"Observation incorrect {vert_loc=} {obs_i=} {var=} {obs_value=} {exp_value=}."
for var in ["U", "V"]:
obs_value = obs[var].values.item()
exp_value = exp[var][vert_loc]
assert np.isclose(
obs_value, exp_value
), f"Observation incorrect {vert_loc=} {i=} {var=} {obs_value=} {exp_value=}."
61 changes: 32 additions & 29 deletions tests/instruments/test_ship_underwater_st.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,49 @@ def test_simulate_ship_underwater_st(tmpdir: py.path.LocalPath) -> None:
# arbitrary time offset for the dummy fieldset
base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d")

# variabes we are going to compare between expected and actual observations
variables = ["salinity", "temperature", "lat", "lon"]

# where to sample
sample_points = [
Spacetime(Location(3, 0), base_time + datetime.timedelta(seconds=0)),
Spacetime(Location(7, 0), base_time + datetime.timedelta(seconds=1)),
Spacetime(Location(1, 2), base_time + datetime.timedelta(seconds=0)),
Spacetime(Location(3, 4), base_time + datetime.timedelta(seconds=1)),
]

# expected observations at sample points
expected_obs = [
{
"salinity": 1,
"temperature": 2,
"lat": 3,
"lon": 0,
"salinity": 5,
"temperature": 6,
"lat": sample_points[0].location.lat,
"lon": sample_points[0].location.lon,
"time": base_time + datetime.timedelta(seconds=0),
},
{
"salinity": 5,
"temperature": 6,
"lat": 7,
"lon": 0,
"salinity": 7,
"temperature": 8,
"lat": sample_points[1].location.lat,
"lon": sample_points[1].location.lon,
"time": base_time + datetime.timedelta(seconds=1),
},
]

# create fieldset based on the expected observations
# indices are time, latitude, longitude
salinity = np.zeros((2, 2, 2))
salinity[0, 0, 0] = expected_obs[0]["salinity"]
salinity[1, 1, 1] = expected_obs[1]["salinity"]

temperature = np.zeros((2, 2, 2))
temperature[0, 0, 0] = expected_obs[0]["temperature"]
temperature[1, 1, 1] = expected_obs[1]["temperature"]

fieldset = FieldSet.from_data(
{
"U": np.zeros((2, 2)),
"V": np.zeros((2, 2)),
"salinity": [
[expected_obs[0]["salinity"], 0],
[0, expected_obs[1]["salinity"]],
],
"temperature": [
[expected_obs[0]["temperature"], 0],
[0, expected_obs[1]["temperature"]],
],
"U": np.zeros((2, 2, 2)),
"V": np.zeros((2, 2, 2)),
"salinity": salinity,
"temperature": temperature,
},
{
"lon": 0,
"lon": np.array([expected_obs[0]["lon"], expected_obs[1]["lon"]]),
"lat": np.array([expected_obs[0]["lat"], expected_obs[1]["lat"]]),
"time": np.array(
[
Expand All @@ -71,6 +71,7 @@ def test_simulate_ship_underwater_st(tmpdir: py.path.LocalPath) -> None:
},
)

# perform simulation
out_path = tmpdir.join("out.zarr")

simulate_ship_underwater_st(
Expand All @@ -80,21 +81,23 @@ def test_simulate_ship_underwater_st(tmpdir: py.path.LocalPath) -> None:
sample_points=sample_points,
)

# test if output is as expected
results = xr.open_zarr(out_path)

# below we assert if output makes sense
assert len(results.trajectory) == 1 # expect a singe trajectory
assert len(results.trajectory) == 1 # expect a single trajectory
traj = results.trajectory.item()
assert len(results.sel(trajectory=traj).obs) == len(
sample_points
) # expect as many obs as sample points

# for every obs, check if the variables match the expected observations
for obs_i, exp in zip(results.sel(trajectory=traj).obs, expected_obs, strict=True):
for i, (obs_i, exp) in enumerate(
zip(results.sel(trajectory=traj).obs, expected_obs, strict=True)
):
obs = results.sel(trajectory=traj, obs=obs_i)
for var in variables:
for var in ["salinity", "temperature", "lat", "lon"]:
obs_value = obs[var].values.item()
exp_value = exp[var]
assert np.isclose(
obs_value, exp_value
), f"Observation incorrect {obs_i=} {var=} {obs_value=} {exp_value=}."
), f"Observation incorrect {i=} {var=} {obs_value=} {exp_value=}."
37 changes: 24 additions & 13 deletions virtual_ship/instruments/adcp.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""ADCP instrument."""

import numpy as np
from parcels import FieldSet, JITParticle, ParticleSet, Variable
import py
from parcels import FieldSet, ParticleSet, ScipyParticle, Variable

from ..spacetime import Spacetime

_ADCPParticle = JITParticle.add_variables(
# we specifically use ScipyParticle because we have many small calls to execute
# there is some overhead with JITParticle and this ends up being significantly faster
_ADCPParticle = ScipyParticle.add_variables(
[
Variable("U", dtype=np.float32, initial=np.nan),
Variable("V", dtype=np.float32, initial=np.nan),
Expand All @@ -21,25 +24,25 @@ def _sample_velocity(particle, fieldset, time):

def simulate_adcp(
fieldset: FieldSet,
out_file_name: str,
out_path: str | py.path.LocalPath,
max_depth: float,
min_depth: float,
bin_size: float,
num_bins: int,
sample_points: list[Spacetime],
) -> None:
"""
Use parcels to simulate an ADCP in a fieldset.

:param fieldset: The fieldset to simulate the ADCP in.
:param out_file_name: The file to write the results to.
:param out_path: The path to write the results to.
:param max_depth: Maximum depth the ADCP can measure.
:param min_depth: Minimum depth the ADCP can measure.
:param bin_size: How many samples to take in the complete range between max_depth and min_depth.
:param num_bins: How many samples to take in the complete range between max_depth and min_depth.
:param sample_points: The places and times to sample at.
"""
sample_points.sort(key=lambda p: p.time)

bins = np.arange(max_depth, min_depth, bin_size)
bins = np.linspace(max_depth, min_depth, num_bins)
num_particles = len(bins)
particleset = ParticleSet.from_list(
fieldset=fieldset,
Expand All @@ -53,14 +56,22 @@ def simulate_adcp(
)

# define output file for the simulation
out_file = particleset.ParticleFile(
name=out_file_name,
)
# outputdt set to infinie as we want to just want to write at the end of every call to 'execute'
out_file = particleset.ParticleFile(name=out_path, outputdt=np.inf)

for point in sample_points:
particleset.lon_nextloop[:] = point.location.lon
particleset.lat_nextloop[:] = point.location.lat
particleset.time_nextloop[:] = point.time
particleset.time_nextloop[:] = fieldset.time_origin.reltime(
np.datetime64(point.time)
)

particleset.execute([_sample_velocity], dt=1, runtime=1, verbose_progress=False)
out_file.write(particleset, time=particleset[0].time)
# perform one step using the particleset
# dt and runtime are set so exactly one step is made.
particleset.execute(
[_sample_velocity],
dt=1,
runtime=1,
verbose_progress=False,
output_file=out_file,
)
18 changes: 9 additions & 9 deletions virtual_ship/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
class Location:
"""A location on a sphere."""

latitude: float
longitude: float
latitude: float

@property
def lat(self) -> float:
def lon(self) -> float:
"""
Shorthand for latitude variable.
Shorthand for longitude variable.

:returns: Latitude variable.
:returns: Longitude variable.
"""
return self.latitude
return self.longitude

@property
def lon(self) -> float:
def lat(self) -> float:
"""
Shorthand for longitude variable.
Shorthand for latitude variable.

:returns: Longitude variable.
:returns: Latitude variable.
"""
return self.longitude
return self.latitude
5 changes: 3 additions & 2 deletions virtual_ship/sailship.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,11 @@ def sailship(config: VirtualShipConfiguration):
print("Simulating onboard ADCP.")
simulate_adcp(
fieldset=config.adcp_fieldset,
out_file_name=os.path.join("results", "adcp.zarr"),
out_path=os.path.join("results", "adcp.zarr"),
max_depth=config.ADCP_settings["max_depth"],
min_depth=-5,
bin_size=config.ADCP_settings["bin_size_m"],
num_bins=(-5 - config.ADCP_settings["max_depth"])
// config.ADCP_settings["bin_size_m"],
sample_points=adcps,
)

Expand Down