Skip to content
8 changes: 4 additions & 4 deletions docs/user_guide/examples/tutorial_unitconverters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"So the U field has a `GeographicPolar` UnitConverter object, the V field has a `Geographic` UnitConverter and the `temp` field has a `UnitConverter` object.\n",
"So the U field has a `GeographicPolar` UnitConverter object, the V field has a `Geographic` UnitConverter and the `temp` field has a `Unity` object.\n",
"\n",
"Indeed, if we multiply the value of the V field with 1852 \\* 60 (the number of meters in 1 degree of latitude), we get the expected 1 m/s.\n"
]
Expand Down Expand Up @@ -248,7 +248,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Indeed, in this case all Fields have the same default `UnitConverter` object.\n"
"Indeed, in this case all Fields have the same default `Unity` object.\n"
]
},
{
Expand Down Expand Up @@ -296,7 +296,7 @@
"kh_meridional_field = parcels.Field(\n",
" \"Kh_meridional\",\n",
" ds[\"Kh_meridional\"],\n",
" grid=grid,\n",
" grid=fieldset.U.grid,\n",
" interp_method=parcels.interpolators.XLinear,\n",
")\n",
"\n",
Expand Down Expand Up @@ -348,7 +348,7 @@
"| `\"V\"` | `Geographic` | $1852 \\cdot 60$ | 1 |\n",
"| `\"Kh_zonal\"` | `GeographicPolarSquare` | $(1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180}))^2$ | 1 |\n",
"| `\"Kh_meridional\"` | `GeographicSquare` | $(1852 \\cdot 60)^2$ | 1 |\n",
"| All other fields | `UnitConverter` | 1 | 1 |\n",
"| All other fields | `Unity` | 1 | 1 |\n",
"\n",
"Only four Field names are recognised and assigned an automatic UnitConverter object. This means that things might go very wrong when e.g. a velocity field is not called `U` or `V`.\n",
"\n",
Expand Down
4 changes: 4 additions & 0 deletions docs/user_guide/v4-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat
## GridSet

- `GridSet` is now a list, so change `fieldset.gridset.grids[0]` to `fieldset.gridset[0]`.

## UnitConverters

- The default `UnitConverter` is now called `Unity()`
4 changes: 2 additions & 2 deletions src/parcels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
GeographicPolar,
GeographicPolarSquare,
GeographicSquare,
UnitConverter,
Unity,
)
from parcels._core.field import Field, VectorField
from parcels._core.fieldset import FieldSet
Expand Down Expand Up @@ -66,7 +66,7 @@
"GeographicPolar",
"GeographicPolarSquare",
"GeographicSquare",
"UnitConverter",
"Unity",
# Status codes and errors
"AllParcelsErrorCodes",
"FieldInterpolationError",
Expand Down
19 changes: 16 additions & 3 deletions src/parcels/_core/converters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from math import pi

import numpy as np
Expand All @@ -11,6 +12,7 @@
"GeographicPolarSquare",
"GeographicSquare",
"UnitConverter",
"Unity",
"_convert_to_flat_array",
"_unitconverters_map",
]
Expand All @@ -27,12 +29,23 @@ def _convert_to_flat_array(var: npt.ArrayLike) -> npt.NDArray:
return np.array(var).flatten()


class UnitConverter:
"""Interface class for spatial unit conversion during field sampling that performs no conversion."""

class UnitConverter(ABC):
source_unit: str | None = None
target_unit: str | None = None

@abstractmethod
def to_target(self, value, z, y, x): ...

@abstractmethod
def to_source(self, value, z, y, x): ...


class Unity(UnitConverter):
"""Interface class for spatial unit conversion during field sampling that performs no conversion."""

source_unit: None
target_unit: None

def to_target(self, value, z, y, x):
return value

Expand Down
3 changes: 2 additions & 1 deletion src/parcels/_core/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from parcels._core.converters import (
UnitConverter,
Unity,
_unitconverters_map,
)
from parcels._core.index_search import GRID_SEARCH_ERROR, LEFT_OUT_OF_BOUNDS, RIGHT_OUT_OF_BOUNDS, _search_time_index
Expand Down Expand Up @@ -135,7 +136,7 @@ def __init__(
self.igrid = -1 # Default the grid index to -1

if self.grid._mesh == "flat" or (self.name not in _unitconverters_map.keys()):
self.units = UnitConverter()
self.units = Unity()
elif self.grid._mesh == "spherical":
self.units = _unitconverters_map[self.name]

Expand Down
4 changes: 2 additions & 2 deletions tests-v3/test_fieldset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Variable,
)
from parcels.field import VectorField
from parcels.tools.converters import GeographicPolar, UnitConverter
from parcels.tools.converters import GeographicPolar, Unity
from tests.utils import TEST_DATA


Expand Down Expand Up @@ -119,7 +119,7 @@ def test_field_from_netcdf_fieldtypes():

# first try without setting fieldtype
fset = FieldSet.from_nemo(filenames, variables, dimensions)
assert isinstance(fset.varU.units, UnitConverter)
assert isinstance(fset.varU.units, Unity)

# now try with setting fieldtype
fset = FieldSet.from_nemo(filenames, variables, dimensions, fieldtype={"varU": "U", "varV": "V"})
Expand Down
20 changes: 19 additions & 1 deletion tests/test_diffusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
import pytest
from scipy import stats

from parcels import Field, FieldSet, Particle, ParticleSet, Variable, VectorField, XGrid
from parcels import (
Field,
FieldSet,
GeographicPolarSquare,
GeographicSquare,
Particle,
ParticleSet,
Unity,
Variable,
VectorField,
XGrid,
)
from parcels._datasets.structured.generated import simple_UV_dataset
from parcels.interpolators import XLinear
from parcels.kernels import AdvectionDiffusionEM, AdvectionDiffusionM1, DiffusionUniformKh
Expand All @@ -28,6 +39,13 @@ def test_fieldKh_Brownian(mesh):
fieldset.add_constant_field("Kh_zonal", kh_zonal, mesh=mesh)
fieldset.add_constant_field("Kh_meridional", kh_meridional, mesh=mesh)

if mesh == "spherical":
assert isinstance(fieldset.Kh_zonal.units, GeographicPolarSquare)
assert isinstance(fieldset.Kh_meridional.units, GeographicSquare)
else:
assert isinstance(fieldset.Kh_zonal.units, Unity)
assert isinstance(fieldset.Kh_meridional.units, Unity)

npart = 100
runtime = np.timedelta64(2, "h")

Expand Down