From 188103112e3374df11f2da04a1962234a23d52c7 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 27 Nov 2025 12:58:16 +0100 Subject: [PATCH 1/6] Adding a new UnitConverter Unity() Following the discussion in #2410 --- src/parcels/__init__.py | 4 ++-- src/parcels/_core/converters.py | 10 ++++++++-- src/parcels/_core/field.py | 3 ++- tests-v3/test_fieldset.py | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/parcels/__init__.py b/src/parcels/__init__.py index 6e0bcfda9..5fba2a72e 100644 --- a/src/parcels/__init__.py +++ b/src/parcels/__init__.py @@ -14,7 +14,7 @@ GeographicPolar, GeographicPolarSquare, GeographicSquare, - UnitConverter, + Unity, ) from parcels._core.field import Field, VectorField from parcels._core.fieldset import FieldSet @@ -66,7 +66,7 @@ "GeographicPolar", "GeographicPolarSquare", "GeographicSquare", - "UnitConverter", + "Unity", # Status codes and errors "AllParcelsErrorCodes", "FieldInterpolationError", diff --git a/src/parcels/_core/converters.py b/src/parcels/_core/converters.py index 35fddc18b..773fbb249 100644 --- a/src/parcels/_core/converters.py +++ b/src/parcels/_core/converters.py @@ -11,6 +11,7 @@ "GeographicPolarSquare", "GeographicSquare", "UnitConverter", + "Unity", "_convert_to_flat_array", "_unitconverters_map", ] @@ -28,11 +29,16 @@ def _convert_to_flat_array(var: npt.ArrayLike) -> npt.NDArray: class UnitConverter: - """Interface class for spatial unit conversion during field sampling that performs no conversion.""" - source_unit: str | None = None target_unit: str | None = None + +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 diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index 37f2dd4af..b8c22ac0a 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -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 @@ -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] diff --git a/tests-v3/test_fieldset.py b/tests-v3/test_fieldset.py index fd83a28be..69295cf87 100644 --- a/tests-v3/test_fieldset.py +++ b/tests-v3/test_fieldset.py @@ -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 @@ -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"}) From a00616ca14f0cfb963987393a2fca18937c76ebb Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 27 Nov 2025 15:31:39 +0100 Subject: [PATCH 2/6] Add Unity() item to migrationguide --- docs/user_guide/v4-migration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user_guide/v4-migration.md b/docs/user_guide/v4-migration.md index 2884049b1..ff6c54606 100644 --- a/docs/user_guide/v4-migration.md +++ b/docs/user_guide/v4-migration.md @@ -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()` From d18ddfa2e9d8e49d4724392eaf797bac32731013 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 27 Nov 2025 15:43:00 +0100 Subject: [PATCH 3/6] Updating unitconverters notebook to use Unity --- docs/user_guide/examples/tutorial_unitconverters.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/examples/tutorial_unitconverters.ipynb b/docs/user_guide/examples/tutorial_unitconverters.ipynb index e9847781f..a28165463 100644 --- a/docs/user_guide/examples/tutorial_unitconverters.ipynb +++ b/docs/user_guide/examples/tutorial_unitconverters.ipynb @@ -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" ] @@ -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" ] }, { @@ -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", From b0d9bdba531b85a830e4e65433672d9e1a93c8e9 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 27 Nov 2025 16:05:56 +0100 Subject: [PATCH 4/6] Add tests for correct UnitConverters in diffusion --- tests/test_diffusion.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_diffusion.py b/tests/test_diffusion.py index 3c9d63a86..246db6822 100644 --- a/tests/test_diffusion.py +++ b/tests/test_diffusion.py @@ -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 @@ -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") From ee3dde415e40368b87feed2c5d35b3ea6cd37c79 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 27 Nov 2025 16:10:15 +0100 Subject: [PATCH 5/6] Fixing small bug in Unitconverters tutorial --- docs/user_guide/examples/tutorial_unitconverters.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/examples/tutorial_unitconverters.ipynb b/docs/user_guide/examples/tutorial_unitconverters.ipynb index a28165463..a89541c43 100644 --- a/docs/user_guide/examples/tutorial_unitconverters.ipynb +++ b/docs/user_guide/examples/tutorial_unitconverters.ipynb @@ -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", From 69fdc4db1b168d34627e0d30be243dabdd251987 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Mon, 1 Dec 2025 09:06:46 +0100 Subject: [PATCH 6/6] Making UnitConverter an abstract base class --- src/parcels/_core/converters.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/parcels/_core/converters.py b/src/parcels/_core/converters.py index 773fbb249..a3ecd965e 100644 --- a/src/parcels/_core/converters.py +++ b/src/parcels/_core/converters.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import ABC, abstractmethod from math import pi import numpy as np @@ -28,10 +29,16 @@ def _convert_to_flat_array(var: npt.ArrayLike) -> npt.NDArray: return np.array(var).flatten() -class UnitConverter: +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."""