|
13 | 13 | "cell_type": "markdown", |
14 | 14 | "metadata": {}, |
15 | 15 | "source": [ |
16 | | - "This tutorial shows how simple it is to construct a Kernel in Parcels that mimics the [vertical movement of Argo floats](https://www.aoml.noaa.gov/phod/argo/images/argo_float_mission.jpg).\n" |
| 16 | + "This tutorial shows how simple it is to construct a Kernel in Parcels that mimics the [vertical movement of Argo floats](https://www.aoml.noaa.gov/phod/argo/images/argo_float_mission.jpg).\n", |
| 17 | + "\n", |
| 18 | + "We first define the kernels for each phase of the Argo cycle." |
17 | 19 | ] |
18 | 20 | }, |
19 | 21 | { |
|
22 | 24 | "metadata": {}, |
23 | 25 | "outputs": [], |
24 | 26 | "source": [ |
25 | | - "# Define the new Kernel that mimics Argo vertical movement\n", |
26 | | - "def ArgoVerticalMovement(particle, fieldset, time):\n", |
27 | | - " driftdepth = 1000 # maximum depth in m\n", |
28 | | - " maxdepth = 2000 # maximum depth in m\n", |
29 | | - " vertical_speed = 0.10 # sink and rise speed in m/s\n", |
30 | | - " cycletime = 10 * 86400 # total time of cycle in seconds\n", |
31 | | - " drifttime = 9 * 86400 # time of deep drift in seconds\n", |
32 | | - "\n", |
33 | | - " if particle.cycle_phase == 0:\n", |
34 | | - " # Phase 0: Sinking with vertical_speed until depth is driftdepth\n", |
35 | | - " particle_ddepth += vertical_speed * particle.dt\n", |
36 | | - " if particle.depth + particle_ddepth >= driftdepth:\n", |
37 | | - " particle_ddepth = driftdepth - particle.depth\n", |
38 | | - " particle.cycle_phase = 1\n", |
39 | | - "\n", |
40 | | - " elif particle.cycle_phase == 1:\n", |
41 | | - " # Phase 1: Drifting at depth for drifttime seconds\n", |
42 | | - " particle.drift_age += particle.dt\n", |
43 | | - " if particle.drift_age >= drifttime:\n", |
44 | | - " particle.drift_age = 0 # reset drift_age for next cycle\n", |
45 | | - " particle.cycle_phase = 2\n", |
46 | | - "\n", |
47 | | - " elif particle.cycle_phase == 2:\n", |
48 | | - " # Phase 2: Sinking further to maxdepth\n", |
49 | | - " particle_ddepth += vertical_speed * particle.dt\n", |
50 | | - " if particle.depth + particle_ddepth >= maxdepth:\n", |
51 | | - " particle_ddepth = maxdepth - particle.depth\n", |
52 | | - " particle.cycle_phase = 3\n", |
53 | | - "\n", |
54 | | - " elif particle.cycle_phase == 3:\n", |
55 | | - " # Phase 3: Rising with vertical_speed until at surface\n", |
56 | | - " particle_ddepth -= vertical_speed * particle.dt\n", |
57 | | - " # particle.temp = fieldset.temp[time, particle.depth, particle.lat, particle.lon] # if fieldset has temperature\n", |
58 | | - " if particle.depth + particle_ddepth <= fieldset.mindepth:\n", |
59 | | - " particle_ddepth = fieldset.mindepth - particle.depth\n", |
60 | | - " # particle.temp = 0./0. # reset temperature to NaN at end of sampling cycle\n", |
61 | | - " particle.cycle_phase = 4\n", |
62 | | - "\n", |
63 | | - " elif particle.cycle_phase == 4:\n", |
64 | | - " # Phase 4: Transmitting at surface until cycletime is reached\n", |
65 | | - " if particle.cycle_age > cycletime:\n", |
66 | | - " particle.cycle_phase = 0\n", |
67 | | - " particle.cycle_age = 0\n", |
68 | | - "\n", |
69 | | - " if particle.state == StatusCode.Evaluate:\n", |
70 | | - " particle.cycle_age += particle.dt # update cycle_age" |
| 27 | + "import numpy as np\n", |
| 28 | + "\n", |
| 29 | + "# Define the new Kernels that mimic Argo vertical movement\n", |
| 30 | + "driftdepth = 1000 # maximum depth in m\n", |
| 31 | + "maxdepth = 2000 # maximum depth in m\n", |
| 32 | + "vertical_speed = 0.10 # sink and rise speed in m/s\n", |
| 33 | + "cycletime = (\n", |
| 34 | + " 10 * 86400\n", |
| 35 | + ") # total time of cycle in seconds # TODO update to \"timedelta64[s]\"\n", |
| 36 | + "drifttime = 9 * 86400 # time of deep drift in seconds\n", |
| 37 | + "\n", |
| 38 | + "\n", |
| 39 | + "def ArgoPhase1(particles, fieldset):\n", |
| 40 | + " dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n", |
| 41 | + "\n", |
| 42 | + " def SinkingPhase(p):\n", |
| 43 | + " \"\"\"Phase 0: Sinking with vertical_speed until depth is driftdepth\"\"\"\n", |
| 44 | + " p.ddepth += vertical_speed * dt\n", |
| 45 | + " p.cycle_phase = np.where(p.depth + p.ddepth >= driftdepth, 1, p.cycle_phase)\n", |
| 46 | + " p.ddepth = np.where(\n", |
| 47 | + " p.depth + p.ddepth >= driftdepth, driftdepth - p.depth, p.ddepth\n", |
| 48 | + " )\n", |
| 49 | + "\n", |
| 50 | + " SinkingPhase(particles[particles.cycle_phase == 0])\n", |
| 51 | + "\n", |
| 52 | + "\n", |
| 53 | + "def ArgoPhase2(particles, fieldset):\n", |
| 54 | + " dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n", |
| 55 | + "\n", |
| 56 | + " def DriftingPhase(p):\n", |
| 57 | + " \"\"\"Phase 1: Drifting at depth for drifttime seconds\"\"\"\n", |
| 58 | + " p.drift_age += dt\n", |
| 59 | + " p.cycle_phase = np.where(p.drift_age >= drifttime, 2, p.cycle_phase)\n", |
| 60 | + " p.drift_age = np.where(p.drift_age >= drifttime, 0, p.drift_age)\n", |
| 61 | + "\n", |
| 62 | + " DriftingPhase(particles[particles.cycle_phase == 1])\n", |
| 63 | + "\n", |
| 64 | + "\n", |
| 65 | + "def ArgoPhase3(particles, fieldset):\n", |
| 66 | + " dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n", |
| 67 | + "\n", |
| 68 | + " def SecondSinkingPhase(p):\n", |
| 69 | + " \"\"\"Phase 2: Sinking further to maxdepth\"\"\"\n", |
| 70 | + " p.ddepth += vertical_speed * dt\n", |
| 71 | + " p.cycle_phase = np.where(p.depth + p.ddepth >= maxdepth, 3, p.cycle_phase)\n", |
| 72 | + " p.ddepth = np.where(\n", |
| 73 | + " p.depth + p.ddepth >= maxdepth, maxdepth - p.depth, p.ddepth\n", |
| 74 | + " )\n", |
| 75 | + "\n", |
| 76 | + " SecondSinkingPhase(particles[particles.cycle_phase == 2])\n", |
| 77 | + "\n", |
| 78 | + "\n", |
| 79 | + "def ArgoPhase4(particles, fieldset):\n", |
| 80 | + " dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n", |
| 81 | + "\n", |
| 82 | + " def RisingPhase(p):\n", |
| 83 | + " \"\"\"Phase 3: Rising with vertical_speed until at surface\"\"\"\n", |
| 84 | + " p.ddepth -= vertical_speed * dt\n", |
| 85 | + " p.temp = fieldset.thetao[p.time, p.depth, p.lat, p.lon]\n", |
| 86 | + " p.cycle_phase = np.where(\n", |
| 87 | + " p.depth + p.ddepth <= fieldset.mindepth, 4, p.cycle_phase\n", |
| 88 | + " )\n", |
| 89 | + " p.ddepth = np.where(\n", |
| 90 | + " p.depth + p.ddepth <= fieldset.mindepth,\n", |
| 91 | + " fieldset.mindepth - p.depth,\n", |
| 92 | + " p.ddepth,\n", |
| 93 | + " )\n", |
| 94 | + "\n", |
| 95 | + " RisingPhase(particles[particles.cycle_phase == 3])\n", |
| 96 | + "\n", |
| 97 | + "\n", |
| 98 | + "def ArgoPhase5(particles, fieldset):\n", |
| 99 | + " def TransmittingPhase(p):\n", |
| 100 | + " \"\"\"Phase 4: Transmitting at surface until cycletime is reached\"\"\"\n", |
| 101 | + " p.cycle_phase = np.where(p.cycle_age >= cycletime, 0, p.cycle_phase)\n", |
| 102 | + " p.cycle_age = np.where(p.cycle_age >= cycletime, 0, p.cycle_age)\n", |
| 103 | + " p.temp = np.nan # no temperature measurement when at surface\n", |
| 104 | + "\n", |
| 105 | + " TransmittingPhase(particles[particles.cycle_phase == 4])\n", |
| 106 | + "\n", |
| 107 | + "\n", |
| 108 | + "def ArgoPhase6(particles, fieldset):\n", |
| 109 | + " dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n", |
| 110 | + " particles.cycle_age += dt # update cycle_age" |
71 | 111 | ] |
72 | 112 | }, |
73 | 113 | { |
|
77 | 117 | "source": [ |
78 | 118 | "And then we can run Parcels with this 'custom kernel'.\n", |
79 | 119 | "\n", |
80 | | - "Note that below we use the two-dimensional velocity fields of GlobCurrent, as these are provided as example_data with Parcels.\n", |
81 | | - "\n", |
82 | | - "We therefore assume that the horizontal velocities are the same throughout the entire water column. However, the `ArgoVerticalMovement` kernel will work on any `FieldSet`, including from full three-dimensional hydrodynamic data.\n", |
83 | | - "\n", |
84 | | - "If the hydrodynamic data also has a Temperature Field, then uncommenting the lines about temperature will also simulate the sampling of temperature.\n" |
| 120 | + "Below we use the horizontal velocity fields of CopernicusMarine, which are provided as example_data with Parcels.\n" |
85 | 121 | ] |
86 | 122 | }, |
87 | 123 | { |
|
92 | 128 | "source": [ |
93 | 129 | "from datetime import timedelta\n", |
94 | 130 | "\n", |
95 | | - "import numpy as np\n", |
| 131 | + "import xarray as xr\n", |
96 | 132 | "\n", |
97 | 133 | "import parcels\n", |
98 | 134 | "\n", |
99 | | - "# Load the GlobCurrent data in the Agulhas region from the example_data\n", |
100 | | - "example_dataset_folder = parcels.download_example_dataset(\"GlobCurrent_example_data\")\n", |
101 | | - "filenames = {\n", |
102 | | - " \"U\": f\"{example_dataset_folder}/20*.nc\",\n", |
103 | | - " \"V\": f\"{example_dataset_folder}/20*.nc\",\n", |
104 | | - "}\n", |
105 | | - "variables = {\n", |
106 | | - " \"U\": \"eastward_eulerian_current_velocity\",\n", |
107 | | - " \"V\": \"northward_eulerian_current_velocity\",\n", |
108 | | - "}\n", |
109 | | - "dimensions = {\"lat\": \"lat\", \"lon\": \"lon\", \"time\": \"time\"}\n", |
110 | | - "fieldset = parcels.FieldSet.from_netcdf(filenames, variables, dimensions)\n", |
111 | | - "# uppermost layer in the hydrodynamic data\n", |
112 | | - "fieldset.mindepth = fieldset.U.depth[0]\n", |
| 135 | + "# Load the CopernicusMarine data in the Agulhas region from the example_datasets\n", |
| 136 | + "example_dataset_folder = parcels.download_example_dataset(\n", |
| 137 | + " \"CopernicusMarine_data_for_Argo_tutorial\"\n", |
| 138 | + ")\n", |
| 139 | + "\n", |
| 140 | + "ds = xr.open_mfdataset(f\"{example_dataset_folder}/*.nc\", combine=\"by_coords\")\n", |
| 141 | + "\n", |
| 142 | + "# TODO check how we can get good performance without loading full dataset in memory\n", |
| 143 | + "ds.load() # load the dataset into memory\n", |
113 | 144 | "\n", |
| 145 | + "fieldset = parcels.FieldSet.from_copernicusmarine(ds)\n", |
| 146 | + "fieldset.add_constant(\"mindepth\", 1.0)\n", |
114 | 147 | "\n", |
115 | 148 | "# Define a new Particle type including extra Variables\n", |
116 | | - "ArgoParticle = parcels.Particle.add_variables(\n", |
| 149 | + "ArgoParticle = parcels.Particle.add_variable(\n", |
117 | 150 | " [\n", |
118 | | - " # Phase of cycle:\n", |
119 | | - " # init_descend=0,\n", |
120 | | - " # drift=1,\n", |
121 | | - " # profile_descend=2,\n", |
122 | | - " # profile_ascend=3,\n", |
123 | | - " # transmit=4\n", |
124 | 151 | " parcels.Variable(\"cycle_phase\", dtype=np.int32, initial=0.0),\n", |
125 | | - " parcels.Variable(\"cycle_age\", dtype=np.float32, initial=0.0),\n", |
| 152 | + " parcels.Variable(\n", |
| 153 | + " \"cycle_age\", dtype=np.float32, initial=0.0\n", |
| 154 | + " ), # TODO update to \"timedelta64[s]\"\n", |
126 | 155 | " parcels.Variable(\"drift_age\", dtype=np.float32, initial=0.0),\n", |
127 | | - " # if fieldset has temperature\n", |
128 | | - " # Variable('temp', dtype=np.float32, initial=np.nan),\n", |
| 156 | + " parcels.Variable(\"temp\", dtype=np.float32, initial=np.nan),\n", |
129 | 157 | " ]\n", |
130 | 158 | ")\n", |
131 | 159 | "\n", |
132 | 160 | "# Initiate one Argo float in the Agulhas Current\n", |
133 | 161 | "pset = parcels.ParticleSet(\n", |
134 | | - " fieldset=fieldset, pclass=ArgoParticle, lon=[32], lat=[-31], depth=[0]\n", |
| 162 | + " fieldset=fieldset,\n", |
| 163 | + " pclass=ArgoParticle,\n", |
| 164 | + " lon=[32],\n", |
| 165 | + " lat=[-31],\n", |
| 166 | + " depth=[fieldset.mindepth],\n", |
135 | 167 | ")\n", |
136 | 168 | "\n", |
137 | 169 | "# combine Argo vertical movement kernel with built-in Advection kernel\n", |
138 | | - "kernels = [ArgoVerticalMovement, parcels.AdvectionRK4]\n", |
| 170 | + "kernels = [\n", |
| 171 | + " ArgoPhase1,\n", |
| 172 | + " ArgoPhase2,\n", |
| 173 | + " ArgoPhase3,\n", |
| 174 | + " ArgoPhase4,\n", |
| 175 | + " ArgoPhase5,\n", |
| 176 | + " ArgoPhase6,\n", |
| 177 | + " parcels.AdvectionRK4,\n", |
| 178 | + "]\n", |
139 | 179 | "\n", |
140 | 180 | "# Create a ParticleFile object to store the output\n", |
141 | | - "output_file = pset.ParticleFile(\n", |
142 | | - " name=\"argo_float\",\n", |
| 181 | + "output_file = parcels.ParticleFile(\n", |
| 182 | + " store=\"argo_float.zarr\",\n", |
143 | 183 | " outputdt=timedelta(minutes=15),\n", |
144 | 184 | " chunks=(1, 500), # setting to write in chunks of 500 observations\n", |
145 | 185 | ")\n", |
|
158 | 198 | "cell_type": "markdown", |
159 | 199 | "metadata": {}, |
160 | 200 | "source": [ |
161 | | - "Now we can plot the trajectory of the Argo float with some simple calls to netCDF4 and matplotlib\n" |
| 201 | + "Now we can plot the trajectory of the Argo float with some simple calls to netCDF4 and matplotlib.\n", |
| 202 | + "\n", |
| 203 | + "First plot the depth as a function of time, with the temperature as color (only on the upcast)." |
| 204 | + ] |
| 205 | + }, |
| 206 | + { |
| 207 | + "cell_type": "code", |
| 208 | + "execution_count": null, |
| 209 | + "metadata": {}, |
| 210 | + "outputs": [], |
| 211 | + "source": [ |
| 212 | + "ds_out = xr.open_zarr(\n", |
| 213 | + " output_file.store, decode_times=False\n", |
| 214 | + ") # TODO fix without using decode_times=False\n", |
| 215 | + "x = ds_out[\"lon\"][:].squeeze()\n", |
| 216 | + "y = ds_out[\"lat\"][:].squeeze()\n", |
| 217 | + "z = ds_out[\"z\"][:].squeeze()\n", |
| 218 | + "time = ds_out[\"time\"][:].squeeze() / 86400 # convert time to days\n", |
| 219 | + "temp = ds_out[\"temp\"][:].squeeze()" |
162 | 220 | ] |
163 | 221 | }, |
164 | 222 | { |
|
168 | 226 | "outputs": [], |
169 | 227 | "source": [ |
170 | 228 | "import matplotlib.pyplot as plt\n", |
171 | | - "import xarray as xr\n", |
172 | | - "from mpl_toolkits.mplot3d import Axes3D\n", |
173 | 229 | "\n", |
174 | | - "ds = xr.open_zarr(\"argo_float.zarr\")\n", |
175 | | - "x = ds[\"lon\"][:].squeeze()\n", |
176 | | - "y = ds[\"lat\"][:].squeeze()\n", |
177 | | - "z = ds[\"z\"][:].squeeze()\n", |
178 | | - "ds.close()\n", |
| 230 | + "fig = plt.figure(figsize=(13, 6))\n", |
| 231 | + "ax = plt.axes()\n", |
| 232 | + "ax.plot(time, z, color=\"gray\")\n", |
| 233 | + "cb = ax.scatter(time, z, c=temp, s=20, marker=\"o\", zorder=2)\n", |
| 234 | + "ax.set_xlabel(\"Time [days]\")\n", |
| 235 | + "ax.set_ylabel(\"Depth (m)\")\n", |
| 236 | + "ax.invert_yaxis()\n", |
| 237 | + "fig.colorbar(cb, label=\"Temperature (°C)\")\n", |
| 238 | + "plt.show()" |
| 239 | + ] |
| 240 | + }, |
| 241 | + { |
| 242 | + "cell_type": "markdown", |
| 243 | + "metadata": {}, |
| 244 | + "source": [ |
| 245 | + "We can also make a 3D plot of the trajectory colored by temperature." |
| 246 | + ] |
| 247 | + }, |
| 248 | + { |
| 249 | + "cell_type": "code", |
| 250 | + "execution_count": null, |
| 251 | + "metadata": {}, |
| 252 | + "outputs": [], |
| 253 | + "source": [ |
| 254 | + "from mpl_toolkits.mplot3d import Axes3D\n", |
179 | 255 | "\n", |
180 | | - "fig = plt.figure(figsize=(13, 10))\n", |
| 256 | + "fig = plt.figure(figsize=(13, 8))\n", |
181 | 257 | "ax = plt.axes(projection=\"3d\")\n", |
182 | | - "cb = ax.scatter(x, y, z, c=z, s=20, marker=\"o\")\n", |
| 258 | + "ax.plot3D(x, y, z, color=\"gray\")\n", |
| 259 | + "cb = ax.scatter(x, y, z, c=temp, s=20, marker=\"o\", zorder=2)\n", |
183 | 260 | "ax.set_xlabel(\"Longitude\")\n", |
184 | 261 | "ax.set_ylabel(\"Latitude\")\n", |
185 | 262 | "ax.set_zlabel(\"Depth (m)\")\n", |
186 | 263 | "ax.set_zlim(np.max(z), 0)\n", |
| 264 | + "fig.colorbar(cb, label=\"Temperature (°C)\")\n", |
187 | 265 | "plt.show()" |
188 | 266 | ] |
189 | 267 | } |
190 | 268 | ], |
191 | 269 | "metadata": { |
192 | 270 | "kernelspec": { |
193 | | - "display_name": "parcels", |
| 271 | + "display_name": "parcels-v4", |
194 | 272 | "language": "python", |
195 | 273 | "name": "python3" |
196 | 274 | }, |
|
204 | 282 | "name": "python", |
205 | 283 | "nbconvert_exporter": "python", |
206 | 284 | "pygments_lexer": "ipython3", |
207 | | - "version": "3.12.3" |
| 285 | + "version": "3.12.11" |
208 | 286 | } |
209 | 287 | }, |
210 | 288 | "nbformat": 4, |
|
0 commit comments