Skip to content

Commit b49404e

Browse files
Merge pull request #2459 from Parcels-code/removing_unitconverters
Removing UnitConverters from v4
2 parents d5009e1 + aeaf27e commit b49404e

16 files changed

+186
-448
lines changed

docs/user_guide/examples/explanation_kernelloop.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Besides having commutable Kernels, the main advantage of this implementation is
4343
Below is a simple example of some particles at the surface of the ocean. We create an idealised zonal wind flow that will "push" a particle that is already affected by the surface currents. The Kernel loop ensures that these two forces act at the same time and location.
4444

4545
```{code-cell}
46+
:tags: [hide-output]
4647
import matplotlib.pyplot as plt
4748
import numpy as np
4849
import xarray as xr
@@ -69,21 +70,18 @@ ds_fields["VWind"] = xr.DataArray(
6970
7071
fieldset = parcels.FieldSet.from_copernicusmarine(ds_fields)
7172
72-
# Set unit converters for custom wind fields
73-
fieldset.UWind.units = parcels.GeographicPolar()
74-
fieldset.VWind.units = parcels.Geographic()
73+
# Create a vecorfield for the wind
74+
windvector = parcels.VectorField("Wind", fieldset.UWind, fieldset.VWind)
75+
fieldset.add_field(windvector)
7576
```
7677

7778
Now we define a wind kernel that uses a forward Euler method to apply the wind forcing. Note that we update the `particles.dlon` and `particles.dlat` variables, rather than `particles.lon` and `particles.lat` directly.
7879

7980
```{code-cell}
8081
def wind_kernel(particles, fieldset):
81-
particles.dlon += (
82-
fieldset.UWind[particles] * particles.dt
83-
)
84-
particles.dlat += (
85-
fieldset.VWind[particles] * particles.dt
86-
)
82+
uwind, vwind = fieldset.Wind[particles]
83+
particles.dlon += uwind * particles.dt
84+
particles.dlat += vwind * particles.dt
8785
```
8886

8987
First run a simulation where we apply kernels as `[AdvectionRK4, wind_kernel]`

docs/user_guide/examples/tutorial_unitconverters.ipynb

Lines changed: 41 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"cell_type": "markdown",
66
"metadata": {},
77
"source": [
8-
"# 🖥️ Spherical grids and unit converters"
8+
"# 🖥️ Spherical grids and velocity conversion"
99
]
1010
},
1111
{
@@ -23,7 +23,7 @@
2323
"cell_type": "markdown",
2424
"metadata": {},
2525
"source": [
26-
"Let's first import the relevant modules, and generate a simple dataset on a 2D spherical mesh, with `U`, `V` and `temperature` data arrays, with the velocities 1 m/s and the temperature 20C.\n"
26+
"Let's first import the relevant modules, and generate a simple dataset on a 2D spherical mesh, with `U`, `V` and `temperature` data arrays, with the velocities 1 m s<sup>-1</sup> and the temperature 20°C.\n"
2727
]
2828
},
2929
{
@@ -67,7 +67,7 @@
6767
"When using a `FieldSet` method for a specific dataset, such as `from_copernicusmarine()`, the grid information is known and parsed by Parcels, so we do not have to add the `mesh` argument.\n",
6868
"```\n",
6969
"\n",
70-
"Plotting the `U` field indeed shows a uniform 1 m/s eastward flow.\n"
70+
"Plotting the `U` field indeed shows a uniform 1 m s<sup>-1</sup> eastward flow.\n"
7171
]
7272
},
7373
{
@@ -104,7 +104,11 @@
104104
"cell_type": "markdown",
105105
"metadata": {},
106106
"source": [
107-
"However, printing the velocites directly shows something perhaps surprising. Here, we use the square-bracket field-interpolation notation to print the field value at (5W, 40N, 0m depth) at time 0. _Note that sampling a velocity in Parcels is done by calling the `fieldset.UV` VectorField; see the [Field Sampling tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_sampling.html#Sampling-velocity-fields) for more information._\n"
107+
"However, printing the velocites directly shows something perhaps surprising. Here, we use the square-bracket field-interpolation notation to print the field value at (5W, 40N, 0m depth) at time 0. \n",
108+
"\n",
109+
"```{note}\n",
110+
"Sampling a velocity in Parcels is done by calling the `fieldset.UV` VectorField; see also the section \"Sampling U and V separately\" below.\n",
111+
"```\n"
108112
]
109113
},
110114
{
@@ -117,38 +121,29 @@
117121
"z = np.array([0])\n",
118122
"lat = np.array([40])\n",
119123
"lon = np.array([-5])\n",
120-
"print(fieldset.UV[time, z, lat, lon])\n",
121-
"print(fieldset.temperature[time, z, lat, lon])"
124+
"u, v = fieldset.UV[time, z, lat, lon]\n",
125+
"temp = fieldset.temperature[time, z, lat, lon]\n",
126+
"\n",
127+
"print(f\"(u, v) = ({u}, {v})\")\n",
128+
"print(f\"temperature = {temp}\")"
122129
]
123130
},
124131
{
125132
"attachments": {},
126133
"cell_type": "markdown",
127134
"metadata": {},
128135
"source": [
129-
"While the temperature field indeed is 20C, as we defined, these printed velocities are much smaller.\n",
136+
"While the temperature field indeed is 20°C, as we defined, these printed velocities are much smaller.\n",
130137
"\n",
131-
"This is because Parcels converts under the hood from m/s to degrees/s. This conversion is done with a `parcels.converters` object, which is stored in the `.units` attribute of each Field. Below, we print these\n"
132-
]
133-
},
134-
{
135-
"cell_type": "code",
136-
"execution_count": null,
137-
"metadata": {},
138-
"outputs": [],
139-
"source": [
140-
"for fld in [fieldset.U, fieldset.V, fieldset.temperature]:\n",
141-
" print(f\"{fld.name}: {fld.units}\")"
138+
"This is because Parcels converts under the hood from m s<sup>-1</sup> to degrees s<sup>-1</sup>."
142139
]
143140
},
144141
{
145142
"attachments": {},
146143
"cell_type": "markdown",
147144
"metadata": {},
148145
"source": [
149-
"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",
150-
"\n",
151-
"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"
146+
"Indeed, if we multiply the value of the U field with 1852 \\* 60 \\* cos(lat) (the number of meters in 1 degree of longitude), we get the expected 1 m s<sup>-1</sup>.\n"
152147
]
153148
},
154149
{
@@ -158,15 +153,30 @@
158153
"outputs": [],
159154
"source": [
160155
"u, v = fieldset.UV[time, z, lat, lon]\n",
161-
"print(v * 1852 * 60)"
156+
"\n",
157+
"u = u * 1852 * 60 * np.cos(np.deg2rad(lat))\n",
158+
"v = v * 1852 * 60 * np.cos(np.deg2rad(lat))\n",
159+
"print(f\"(u, v) = ({u}, {v})\")\n",
160+
"\n",
161+
"assert np.isclose(u, 1.0)\n",
162+
"assert np.isclose(v, 1.0)"
163+
]
164+
},
165+
{
166+
"cell_type": "markdown",
167+
"metadata": {},
168+
"source": [
169+
"```{note}\n",
170+
"It may be surprising that the conversion factor depends on latitude also for the V velocity component. This is because Parcels assumes a flux-based interpolation, so that the velocity components are defined on the faces of the grid cells, which vary in size with latitude.\n",
171+
"```"
162172
]
163173
},
164174
{
165175
"attachments": {},
166176
"cell_type": "markdown",
167177
"metadata": {},
168178
"source": [
169-
"Note that you can also interpolate the Field without a unit conversion, by using the `eval()` method and setting `applyConversion=False`, as below\n"
179+
"You can also interpolate the Field to these values directly by using the `eval()` method and setting `applyConversion=False`, as below\n"
170180
]
171181
},
172182
{
@@ -187,134 +197,17 @@
187197
]
188198
},
189199
{
190-
"attachments": {},
191-
"cell_type": "markdown",
192-
"metadata": {},
193-
"source": [
194-
"## UnitConverters for `mesh='flat'`\n"
195-
]
196-
},
197-
{
198-
"attachments": {},
199-
"cell_type": "markdown",
200-
"metadata": {},
201-
"source": [
202-
"If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the XGrid object.\n"
203-
]
204-
},
205-
{
206-
"cell_type": "code",
207-
"execution_count": null,
208-
"metadata": {},
209-
"outputs": [],
210-
"source": [
211-
"ds_flat = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"flat\").isel(time=0, depth=0)\n",
212-
"ds_flat[\"temperature\"] = ds_flat[\"U\"] + 20 # add temperature field of 20 deg\n",
213-
"ds_flat[\"U\"].data[:] = 1.0 # set U to 1 m/s\n",
214-
"ds_flat[\"V\"].data[:] = 1.0 # set V to 1 m/s\n",
215-
"grid = parcels.XGrid.from_dataset(ds_flat, mesh=\"flat\")\n",
216-
"U = parcels.Field(\"U\", ds_flat[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
217-
"V = parcels.Field(\"V\", ds_flat[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
218-
"UV = parcels.VectorField(\"UV\", U, V)\n",
219-
"temperature = parcels.Field(\n",
220-
" \"temperature\",\n",
221-
" ds_flat[\"temperature\"],\n",
222-
" grid,\n",
223-
" interp_method=parcels.interpolators.XLinear,\n",
224-
")\n",
225-
"fieldset_flat = parcels.FieldSet([U, V, UV, temperature])\n",
226-
"\n",
227-
"plt.pcolormesh(\n",
228-
" fieldset_flat.U.grid.lon,\n",
229-
" fieldset_flat.U.grid.lat,\n",
230-
" fieldset_flat.U.data[0, 0, :, :],\n",
231-
" vmin=0,\n",
232-
" vmax=1,\n",
233-
" shading=\"gouraud\",\n",
234-
")\n",
235-
"plt.colorbar()\n",
236-
"plt.show()\n",
237-
"\n",
238-
"print(\n",
239-
" \"Velocities:\",\n",
240-
" fieldset_flat.UV[time, z, lat, lon],\n",
241-
")\n",
242-
"for fld in [fieldset_flat.U, fieldset_flat.V, fieldset_flat.temperature]:\n",
243-
" print(f\"{fld.name}: {fld.units}\")"
244-
]
245-
},
246-
{
247-
"attachments": {},
248-
"cell_type": "markdown",
249-
"metadata": {},
250-
"source": [
251-
"Indeed, in this case all Fields have the same default `Unity` object.\n"
252-
]
253-
},
254-
{
255-
"attachments": {},
256-
"cell_type": "markdown",
257-
"metadata": {},
258-
"source": [
259-
"## UnitConverters for Diffusion fields\n"
260-
]
261-
},
262-
{
263-
"attachments": {},
264200
"cell_type": "markdown",
265201
"metadata": {},
266202
"source": [
267-
"The units for Brownian diffusion are in $m^2/s$. If (and only if!) the diffusion fields are called \"Kh_zonal\" and \"Kh_meridional\", Parcels will automatically assign the correct Unitconverter objects to these fields.\n"
268-
]
269-
},
270-
{
271-
"cell_type": "code",
272-
"execution_count": null,
273-
"metadata": {},
274-
"outputs": [],
275-
"source": [
276-
"kh_zonal = 100 # in m^2/s\n",
277-
"kh_meridional = 100 # in m^2/s\n",
278-
"\n",
279-
"ds[\"Kh_zonal\"] = xr.DataArray(\n",
280-
" data=kh_zonal * np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n",
281-
")\n",
282-
"\n",
283-
"kh_zonal_field = parcels.Field(\n",
284-
" \"Kh_zonal\",\n",
285-
" ds[\"Kh_zonal\"],\n",
286-
" grid=fieldset.U.grid,\n",
287-
" interp_method=parcels.interpolators.XLinear,\n",
288-
")\n",
289-
"\n",
290-
"fieldset.add_field(kh_zonal_field)\n",
291-
"\n",
292-
"ds[\"Kh_meridional\"] = xr.DataArray(\n",
293-
" data=kh_meridional * np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n",
294-
")\n",
295-
"\n",
296-
"kh_meridional_field = parcels.Field(\n",
297-
" \"Kh_meridional\",\n",
298-
" ds[\"Kh_meridional\"],\n",
299-
" grid=fieldset.U.grid,\n",
300-
" interp_method=parcels.interpolators.XLinear,\n",
301-
")\n",
302-
"\n",
303-
"fieldset.add_field(kh_meridional_field)\n",
304-
"\n",
305-
"for fld in [fieldset.Kh_zonal, fieldset.Kh_meridional]:\n",
306-
" val = fld[time, z, lat, lon]\n",
307-
" print(f\"{fld.name}: {val} {fld.units}\")"
203+
"## Don't sample U and V separately"
308204
]
309205
},
310206
{
311-
"attachments": {},
312207
"cell_type": "markdown",
313208
"metadata": {},
314209
"source": [
315-
"Here, the unitconverters are `GeographicPolarSquare` and `GeographicSquare`, respectively.\n",
316-
"\n",
317-
"Indeed, multiplying with $(1852\\cdot60)^2$ returns the original value\n"
210+
"Sampling `U` and `V` separately will _not_ convert to degrees s<sup>-1</sup>, so these velocities cannot be used directly for advection on spherical coordinates. This is one of the main reasons to always use the `UV` VectorField for velocity sampling in Parcels.\n"
318211
]
319212
},
320213
{
@@ -323,76 +216,21 @@
323216
"metadata": {},
324217
"outputs": [],
325218
"source": [
326-
"deg_to_m = 1852 * 60\n",
327-
"print(fieldset.Kh_meridional[time, z, lat, lon] * deg_to_m**2)"
328-
]
329-
},
330-
{
331-
"attachments": {},
332-
"cell_type": "markdown",
333-
"metadata": {},
334-
"source": [
335-
"## Adding a UnitConverter object to a Field\n"
219+
"for fld in [fieldset.U, fieldset.V]:\n",
220+
" print(f\"{fld.name}: {fld.eval(time, z, lat, lon)}\")"
336221
]
337222
},
338223
{
339-
"attachments": {},
340224
"cell_type": "markdown",
341225
"metadata": {},
342226
"source": [
343-
"So, to summarise, here is a table with all the conversions\n",
344-
"\n",
345-
"| Field name | Converter object | Conversion for `mesh='spherical'` | Conversion for `mesh='flat'` |\n",
346-
"| ---------------- | ----------------------- | --------------------------------------------------------- | ---------------------------- |\n",
347-
"| `\"U\"` | `GeographicPolar` | $1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180})$ | 1 |\n",
348-
"| `\"V\"` | `Geographic` | $1852 \\cdot 60$ | 1 |\n",
349-
"| `\"Kh_zonal\"` | `GeographicPolarSquare` | $(1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180}))^2$ | 1 |\n",
350-
"| `\"Kh_meridional\"` | `GeographicSquare` | $(1852 \\cdot 60)^2$ | 1 |\n",
351-
"| All other fields | `Unity` | 1 | 1 |\n",
227+
"## Unit conversion for other fields such as diffusivity\n",
352228
"\n",
353-
"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",
229+
"For other fields such as diffusivity, Parcels does not apply similar unit conversions when using a `spherical` mesh. For example, if we define a diffusivity field with value 10 m<sup>2</sup> s<sup>-1</sup>, Parcels will not convert this to degrees<sup>2</sup> s<sup>-1</sup> under the hood. \n",
354230
"\n",
355-
"Fortunately, you can always add a UnitConverter later, as explained below:\n"
356-
]
357-
},
358-
{
359-
"cell_type": "code",
360-
"execution_count": null,
361-
"metadata": {},
362-
"outputs": [],
363-
"source": [
364-
"ds[\"Ustokes\"] = xr.DataArray(\n",
365-
" data=np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n",
366-
")\n",
231+
"If you want to work with diffusivity in degrees<sup>2</sup> s<sup>-1</sup> (for example to move particles using a random walk), you will have to convert this yourself in your kernel. \n",
367232
"\n",
368-
"fieldset.add_field(\n",
369-
" parcels.Field(\n",
370-
" \"Ustokes\",\n",
371-
" ds[\"Ustokes\"],\n",
372-
" grid=fieldset.U.grid,\n",
373-
" interp_method=parcels.interpolators.XLinear,\n",
374-
" )\n",
375-
")\n",
376-
"print(fieldset.Ustokes[time, z, lat, lon])"
377-
]
378-
},
379-
{
380-
"attachments": {},
381-
"cell_type": "markdown",
382-
"metadata": {},
383-
"source": [
384-
"This value for `Ustokes` of course is not as expected, since the mesh is spherical and hence this would mean 1 degree/s velocity. Assigning the correct `GeographicPolar` Unitconverter gives\n"
385-
]
386-
},
387-
{
388-
"cell_type": "code",
389-
"execution_count": null,
390-
"metadata": {},
391-
"outputs": [],
392-
"source": [
393-
"fieldset.Ustokes.units = parcels.GeographicPolar()\n",
394-
"print(fieldset.Ustokes[time, z, lat, lon])\n",
395-
"print(fieldset.Ustokes[time, z, lat, lon] * 1852 * 60 * np.cos(40 * np.pi / 180))"
233+
"Note that for the built-in `DiffusionUniformKh`, `AdvectionDiffusionM1` and `AdvectionDiffusionEM`, the conversion is done automatically."
396234
]
397235
}
398236
],

docs/user_guide/examples_v3/example_moving_eddies.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ def moving_eddies_fieldset(xdim=200, ydim=350, mesh="flat"):
2525
xdim :
2626
Vertical dimension of the generated fieldset (Default value = 200)
2727
mesh : str
28-
String indicating the type of mesh coordinates and
29-
units used during velocity interpolation:
28+
String indicating the type of mesh coordinates used during velocity interpolation:
3029
3130
1. spherical: Lat and lon in degree, with a
3231
correction for zonal velocity U near the poles.

docs/user_guide/examples_v3/example_peninsula.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ def peninsula_fieldset(xdim, ydim, mesh="flat", grid_type="A"):
2424
xdim :
2525
Vertical dimension of the generated fieldset
2626
mesh : str
27-
String indicating the type of mesh coordinates and
28-
units used during velocity interpolation:
27+
String indicating the type of mesh coordinates used during velocity interpolation:
2928
3029
1. spherical: Lat and lon in degree, with a
3130
correction for zonal velocity U near the poles.

0 commit comments

Comments
 (0)