From 6751ac96efeab17fb7fd53285fb70a140d3d1981 Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Thu, 20 Nov 2025 14:33:41 +0100 Subject: [PATCH 1/9] update kernelloop tutorial --- .../examples/tutorial_kernelloop.ipynb | 594 ++++++++++++++++++ .../examples_v3/tutorial_kernelloop.ipynb | 377 ----------- 2 files changed, 594 insertions(+), 377 deletions(-) create mode 100644 docs/user_guide/examples/tutorial_kernelloop.ipynb delete mode 100644 docs/user_guide/examples_v3/tutorial_kernelloop.ipynb diff --git a/docs/user_guide/examples/tutorial_kernelloop.ipynb b/docs/user_guide/examples/tutorial_kernelloop.ipynb new file mode 100644 index 0000000000..bc4b863d79 --- /dev/null +++ b/docs/user_guide/examples/tutorial_kernelloop.ipynb @@ -0,0 +1,594 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The Parcels Kernel loop\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial explains how Parcels executes multiple Kernels, and what happens under the hood when you combine Kernels. \n", + "\n", + "This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle.\n", + "\n", + "In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic implementation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below is a structured overview of the Kernel loop is implemented. Note that this is for `lon` only, but the same process is applied for `lat` and `z`.\n", + "\n", + "1. Initialise an extra Variable `particles.dlon=0`\n", + "\n", + "2. Within the Kernel loop, for each particle:
\n", + "\n", + " 1. Update `particles.lon += particles.dlon`
\n", + "\n", + " 2. Update `particles.time += particles.dt` (except for on the first iteration of the Kernel loop)
\n", + "\n", + " 3. Set variable `particles.dlon = 0`
\n", + "\n", + " 4. For each Kernel in the list of Kernels:\n", + " \n", + " 1. Execute the Kernel\n", + " \n", + " 2. Update `particles.dlon` by adding the change in longitude, if needed
\n", + "\n", + " 5. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file
\n", + "\n", + "Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particle.temp = fieldset.Temp[particle.time, particle.z, particle.lat, particle.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example use" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.8.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.2/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.8.0.min.js\", \"https://cdn.holoviz.org/panel/1.8.2/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "6ef4fa27-3f74-4b8e-9367-db5920d64218" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const py_version = '3.8.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.2/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/fd/p7j_05zx409dzmtfjjjnjgfh0000gn/T/ipykernel_19079/1981051754.py:7: UserWarning: This is an alpha version of Parcels v4. The API is not stable and may change without deprecation warnings.\n", + " import parcels\n" + ] + } + ], + "source": [ + "from datetime import timedelta\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "import parcels" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: cf_xarray found variable 'uo' with CF standard name 'eastward_sea_water_velocity' in dataset, renamed it to 'U' for Parcels simulation.\n", + "INFO: cf_xarray found variable 'vo' with CF standard name 'northward_sea_water_velocity' in dataset, renamed it to 'V' for Parcels simulation.\n" + ] + } + ], + "source": [ + "# Load the CopernicusMarine data in the Agulhas region from the example_datasets\n", + "example_dataset_folder = parcels.download_example_dataset(\n", + " \"CopernicusMarine_data_for_Argo_tutorial\"\n", + ")\n", + "\n", + "ds_fields = xr.open_mfdataset(f\"{example_dataset_folder}/*.nc\", combine=\"by_coords\")\n", + "ds_fields.load() # load the dataset into memory\n", + "\n", + "# Create an idealised wind field and add it to the fieldset\n", + "tdim, ydim, xdim = (len(ds_fields.time),len(ds_fields.latitude), len(ds_fields.longitude))\n", + "ds_fields[\"UWind\"] = xr.DataArray(\n", + " data=np.ones((tdim, ydim, xdim)) * np.sin(ds_fields.latitude.values)[None, :, None],\n", + " coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])\n", + "\n", + "ds_fields[\"VWind\"] = xr.DataArray(\n", + " data=np.zeros((tdim, ydim, xdim)),\n", + " coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])\n", + "\n", + "fieldset = parcels.FieldSet.from_copernicusmarine(ds_fields)\n", + "\n", + "fieldset.UWind.units = parcels.GeographicPolar()\n", + "fieldset.VWind.units = parcels.Geographic()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now define a wind kernel that uses a forward Euler method to apply the wind forcing. Note that we update the `particle_dlon` and `particle_dlat` variables, rather than `particle.lon` and `particle.lat` directly." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def wind_kernel(particles, fieldset):\n", + " dt_float = particles.dt / np.timedelta64(1, 's')\n", + " particles.dlon += (\n", + " fieldset.UWind[particles] * dt_float\n", + " )\n", + " particles.dlat += (\n", + " fieldset.VWind[particles] * dt_float\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run a simulation where we apply first kernels as `[AdvectionRK4, wind_kernel]`" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in /Users/Gebruiker/Documents/UU/parcels/Parcels/docs/user_guide/examples/advection_then_wind.zarr\n", + "Integration time: 2024-01-05T18:00:11.128799232: 100%|██████████| 432000.0/432000.0 [00:01<00:00, 368600.70it/s]\n" + ] + } + ], + "source": [ + "npart = 10\n", + "z = np.repeat(ds_fields.depth[0].values, npart)\n", + "lons = np.repeat(31, npart)\n", + "lats = np.linspace(-32.5, -30.5, npart)\n", + "\n", + "pset = parcels.ParticleSet(fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons)\n", + "output_file = parcels.ParticleFile(\n", + " store=\"advection_then_wind.zarr\", outputdt=timedelta(hours=6)\n", + ")\n", + "pset.execute(\n", + " [parcels.kernels.AdvectionRK4, wind_kernel],\n", + " runtime=timedelta(days=5),\n", + " dt=timedelta(hours=1),\n", + " output_file=output_file,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in /Users/Gebruiker/Documents/UU/parcels/Parcels/docs/user_guide/examples/wind_then_advection.zarr\n", + "Integration time: 2024-01-05T18:00:11.128799232: 100%|██████████| 432000.0/432000.0 [00:01<00:00, 331309.93it/s]\n" + ] + } + ], + "source": [ + "pset_reverse = parcels.ParticleSet(\n", + " fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons\n", + ")\n", + "output_file_reverse = parcels.ParticleFile(\n", + " store=\"wind_then_advection.zarr\", outputdt=timedelta(hours=6)\n", + ")\n", + "pset_reverse.execute(\n", + " [wind_kernel, parcels.kernels.AdvectionRK4],\n", + " runtime=timedelta(days=5),\n", + " dt=timedelta(hours=1),\n", + " output_file=output_file_reverse,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, plot the trajectories to show that they are identical in the two simulations." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the resulting particle trajectories overlapped for both cases\n", + "advection_then_wind = xr.open_zarr(\"advection_then_wind.zarr\")\n", + "wind_then_advection = xr.open_zarr(\"wind_then_advection.zarr\")\n", + "plt.plot(wind_then_advection.lon.T, wind_then_advection.lat.T, \"-\")\n", + "plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, \"--\", c=\"k\", alpha=0.7)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Caveat: Avoid updating particle locations directly in Kernels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is better not to update `particle.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particle.lon` in a Kernel will throw a warning. \n", + "\n", + "Instead, update the local variable `particle.dlon`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with Status Codes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to capture errors in the Kernel loop, Parcels uses a Status Code system. There are several Status Codes, listed below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Success = 0\n", + "EndofLoop = 1\n", + "Evaluate = 10\n", + "Repeat = 20\n", + "Delete = 30\n", + "StopExecution = 40\n", + "StopAllExecution = 41\n", + "Error = 50\n", + "ErrorInterpolation = 51\n", + "ErrorGridSearching = 52\n", + "ErrorOutOfBounds = 60\n", + "ErrorThroughSurface = 61\n", + "ErrorOutsideTimeInterval = 70\n" + ] + } + ], + "source": [ + "from parcels import StatusCode\n", + "\n", + "for statuscode, val in StatusCode.__dict__.items():\n", + " if statuscode.startswith(\"__\"):\n", + " continue\n", + " print(f\"{statuscode} = {val}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once an error is thrown (for example, a Field Interpolation error), then the `particle.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. \n", + "\n", + "For example, you can write a Kernel that checks for `particle.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def CheckOutOfBounds(particles, fieldset):\n", + " if particles.state == StatusCode.ErrorOutOfBounds:\n", + " particles.delete()\n", + "\n", + "\n", + "def CheckError(particles, fieldset):\n", + " if particles.state >= 50: # This captures all Errors\n", + " particles.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particle.state = StatusCode.Success` yourself. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def Move1DegreeWest(particles, fieldset):\n", + " if particles.state == StatusCode.ErrorOutOfBounds:\n", + " particles.dlon -= 1.0\n", + " particles.state = StatusCode.Success" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or, if you want to make sure that particles don't escape through the water surface" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def KeepInOcean(particles, fieldset):\n", + " if particles.state == StatusCode.ErrorThroughSurface:\n", + " particles.dz = 0.0\n", + " particles.state = StatusCode.Success" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. \n", + "\n", + "Note that these Kernels that control what to do with `particle.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particle.state` or the `particle_dlon` variables." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "test-notebooks", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/user_guide/examples_v3/tutorial_kernelloop.ipynb b/docs/user_guide/examples_v3/tutorial_kernelloop.ipynb deleted file mode 100644 index e10d2c4101..0000000000 --- a/docs/user_guide/examples_v3/tutorial_kernelloop.ipynb +++ /dev/null @@ -1,377 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Parcels Kernel loop\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial explains how Parcels executes multiple Kernels, and what happens under the hood when you combine Kernels. \n", - "\n", - "This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle.\n", - "\n", - "In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Basic implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Below is a structured overview of the Kernel loop is implemented. Note that this is for `lon` only, but the same process is applied for `lat` and `z`.\n", - "\n", - "1. Initialise an extra Variable `particles.dlon=0`\n", - "\n", - "2. Within the Kernel loop, for each particle:
\n", - "\n", - " 1. Update `particles.lon += particles.dlon`
\n", - "\n", - " 2. Update `particles.time += particles.dt` (except for on the first iteration of the Kernel loop)
\n", - "\n", - " 3. Set variable `particles.dlon = 0`
\n", - "\n", - " 4. For each Kernel in the list of Kernels:\n", - " \n", - " 1. Execute the Kernel\n", - " \n", - " 2. Update `particles.dlon` by adding the change in longitude, if needed
\n", - "\n", - " 5. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file
\n", - "\n", - "Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particle.temp = fieldset.Temp[particle.time, particle.z, particle.lat, particle.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example use" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "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." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import timedelta\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import xarray as xr\n", - "\n", - "import parcels\n", - "\n", - "# Load the GlobCurrent data in the Agulhas region from the example_data\n", - "example_dataset_folder = parcels.download_example_dataset(\"GlobCurrent_example_data\")\n", - "filenames = {\n", - " \"U\": f\"{example_dataset_folder}/20*.nc\",\n", - " \"V\": f\"{example_dataset_folder}/20*.nc\",\n", - "}\n", - "variables = {\n", - " \"U\": \"eastward_eulerian_current_velocity\",\n", - " \"V\": \"northward_eulerian_current_velocity\",\n", - "}\n", - "dimensions = {\"lat\": \"lat\", \"lon\": \"lon\", \"time\": \"time\"}\n", - "fieldset = parcels.FieldSet.from_netcdf(\n", - " filenames, variables, dimensions, allow_time_extrapolation=True\n", - ")\n", - "# uppermost layer in the hydrodynamic data\n", - "fieldset.mindepth = fieldset.U.depth[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create an idealised wind field and add it to the fieldset\n", - "xdim, ydim = (len(fieldset.U.lon), len(fieldset.U.lat))\n", - "UWind = parcels.Field(\n", - " \"UWind\",\n", - " np.ones((ydim, xdim), dtype=np.float32) * np.sin(fieldset.U.lat)[:, None],\n", - " lon=fieldset.U.lon,\n", - " lat=fieldset.U.lat,\n", - " mesh=\"spherical\",\n", - ")\n", - "VWind = parcels.Field(\n", - " \"VWind\",\n", - " np.zeros((ydim, xdim), dtype=np.float32),\n", - " grid=UWind.grid,\n", - ")\n", - "UWind.units = parcels.tools.converters.GeographicPolar()\n", - "VWind.units = parcels.tools.converters.Geographic()\n", - "\n", - "fieldset_wind = parcels.FieldSet(UWind, VWind)\n", - "\n", - "fieldset.add_field(fieldset_wind.U, name=\"UWind\")\n", - "fieldset.add_field(fieldset_wind.V, name=\"VWind\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now define a wind kernel that uses a forward Euler method to apply the wind forcing. Note that we update the `particle_dlon` and `particle_dlat` variables, rather than `particle.lon` and `particle.lat` directly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def wind_kernel(particle, fieldset, time):\n", - " particle_dlon += (\n", - " fieldset.UWind[time, particle.z, particle.lat, particle.lon] * particle.dt\n", - " )\n", - " particle_dlat += (\n", - " fieldset.VWind[time, particle.z, particle.lat, particle.lon] * particle.dt\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Run a simulation where we apply first kernels as `[AdvectionRK4, wind_kernel]`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "lons = 26.0 * np.ones(10)\n", - "lats = np.linspace(-37.5, -34.5, 10)\n", - "\n", - "pset = parcels.ParticleSet(fieldset, pclass=parcels.Particle, lon=lons, lat=lats)\n", - "output_file = pset.ParticleFile(\n", - " name=\"advection_then_wind.zarr\", outputdt=timedelta(hours=6)\n", - ")\n", - "pset.execute(\n", - " [parcels.kernels.AdvectionRK4, wind_kernel],\n", - " runtime=timedelta(days=5),\n", - " dt=timedelta(hours=1),\n", - " output_file=output_file,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pset_reverse = parcels.ParticleSet(\n", - " fieldset, pclass=parcels.Particle, lon=lons, lat=lats\n", - ")\n", - "output_file_reverse = pset_reverse.ParticleFile(\n", - " name=\"wind_then_advection.zarr\", outputdt=timedelta(hours=6)\n", - ")\n", - "pset_reverse.execute(\n", - " [wind_kernel, parcels.kernels.AdvectionRK4],\n", - " runtime=timedelta(days=5),\n", - " dt=timedelta(hours=1),\n", - " output_file=output_file_reverse,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, plot the trajectories to show that they are identical in the two simulations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot the resulting particle trajectories overlapped for both cases\n", - "advection_then_wind = xr.open_zarr(\"advection_then_wind.zarr\")\n", - "wind_then_advection = xr.open_zarr(\"wind_then_advection.zarr\")\n", - "plt.plot(wind_then_advection.lon.T, wind_then_advection.lat.T, \"-\")\n", - "plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, \"--\", c=\"k\", alpha=0.7)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Caveat: Avoid updating particle locations directly in Kernels" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is better not to update `particle.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particle.lon` in a Kernel will throw a warning. \n", - "\n", - "Instead, update the local variable `particle.dlon`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with Status Codes" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to capture errors in the Kernel loop, Parcels uses a Status Code system. There are several Status Codes, listed below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from parcels import StatusCode\n", - "\n", - "for statuscode, val in StatusCode.__dict__.items():\n", - " if statuscode.startswith(\"__\"):\n", - " continue\n", - " print(f\"{statuscode} = {val}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once an error is thrown (for example, a Field Interpolation error), then the `particle.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. \n", - "\n", - "For example, you can write a Kernel that checks for `particle.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def CheckOutOfBounds(particle, fieldset, time):\n", - " if particle.state == StatusCode.ErrorOutOfBounds:\n", - " particle.delete()\n", - "\n", - "\n", - "def CheckError(particle, fieldset, time):\n", - " if particle.state >= 50: # This captures all Errors\n", - " particle.delete()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particle.state = StatusCode.Success` yourself. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def Move1DegreeWest(particle, fieldset, time):\n", - " if particle.state == StatusCode.ErrorOutOfBounds:\n", - " particle_dlon -= 1.0\n", - " particle.state = StatusCode.Success" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or, if you want to make sure that particles don't escape through the water surface" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def KeepInOcean(particle, fieldset, time):\n", - " if particle.state == StatusCode.ErrorThroughSurface:\n", - " particle_dz = 0.0\n", - " particle.state = StatusCode.Success" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. \n", - "\n", - "Note that these Kernels that control what to do with `particle.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particle.state` or the `particle_dlon` variables." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "parcels", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} From f979f906b8b81083b6ae3fc5cefb10a59c39d31d Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Thu, 20 Nov 2025 15:27:50 +0100 Subject: [PATCH 2/9] move tutorial.ipynb to explanation.md --- .../examples/explanation_kernelloop.md | 193 ++++++ .../examples/tutorial_kernelloop.ipynb | 594 ------------------ 2 files changed, 193 insertions(+), 594 deletions(-) create mode 100644 docs/user_guide/examples/explanation_kernelloop.md delete mode 100644 docs/user_guide/examples/tutorial_kernelloop.ipynb diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md new file mode 100644 index 0000000000..e445e961f3 --- /dev/null +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -0,0 +1,193 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +--- + +# The Parcels Kernel loop + +This tutorial explains how Parcels executes multiple Kernels, and what happens under the hood when you combine Kernels. + +This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels! + +## Background + +When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle. + +In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement. + +## Basic implementation + +Below is a structured overview of the Kernel loop is implemented. Note that this is for `lon` only, but the same process is applied for `lat` and `z`. + +1. Initialise an extra Variable `particles.dlon=0` + +2. Within the Kernel loop, for each particle: + 1. Update `particles.lon += particles.dlon` + + 2. Update `particles.time += particles.dt` (except for on the first iteration of the Kernel loop)
+ + 3. Set variable `particles.dlon = 0` + + 4. For each Kernel in the list of Kernels: + 1. Execute the Kernel + + 2. Update `particles.dlon` by adding the change in longitude, if needed + + 5. If `outputdt` is a multiple of `particles.time`, write `particles.lon` and `particles.time` to zarr output file + +Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particles.temp = fieldset.Temp[particles.time, particles.z, particles.lat, particles.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file. + +## Example with multiple Kernels + +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, as we will show. + +```{code-cell} +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr + +import parcels + +# Load the CopernicusMarine data in the Agulhas region from the example_datasets +example_dataset_folder = parcels.download_example_dataset( + "CopernicusMarine_data_for_Argo_tutorial" +) + +ds_fields = xr.open_mfdataset(f"{example_dataset_folder}/*.nc", combine="by_coords") +ds_fields.load() # load the dataset into memory + +# Create an idealised wind field and add it to the dataset +tdim, ydim, xdim = (len(ds_fields.time),len(ds_fields.latitude), len(ds_fields.longitude)) +ds_fields["UWind"] = xr.DataArray( + data=np.ones((tdim, ydim, xdim)) * np.sin(ds_fields.latitude.values)[None, :, None], + coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude]) + +ds_fields["VWind"] = xr.DataArray( + data=np.zeros((tdim, ydim, xdim)), + coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude]) + +fieldset = parcels.FieldSet.from_copernicusmarine(ds_fields) + +# Set unit converters for custom wind fields +fieldset.UWind.units = parcels.GeographicPolar() +fieldset.VWind.units = parcels.Geographic() +``` + +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. + +```{code-cell} +def wind_kernel(particles, fieldset): + dt_float = particles.dt / np.timedelta64(1, 's') + particles.dlon += ( + fieldset.UWind[particles] * dt_float + ) + particles.dlat += ( + fieldset.VWind[particles] * dt_float + ) +``` + +Run a simulation where we apply first kernels as `[AdvectionRK4, wind_kernel]` + +```{code-cell} +:tags: [hide-output] +npart = 10 +z = np.repeat(ds_fields.depth[0].values, npart) +lons = np.repeat(31, npart) +lats = np.linspace(-32.5, -30.5, npart) + +pset = parcels.ParticleSet(fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons) +output_file = parcels.ParticleFile( + store="advection_then_wind.zarr", outputdt=timedelta(hours=6) +) +pset.execute( + [parcels.kernels.AdvectionRK4, wind_kernel], + runtime=timedelta(days=5), + dt=timedelta(hours=1), + output_file=output_file, +) +``` + +And also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]` + +```{code-cell} +pset_reverse = parcels.ParticleSet( + fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons +) +output_file_reverse = parcels.ParticleFile( + store="wind_then_advection.zarr", outputdt=timedelta(hours=6) +) +pset_reverse.execute( + [wind_kernel, parcels.kernels.AdvectionRK4], + runtime=timedelta(days=5), + dt=timedelta(hours=1), + output_file=output_file_reverse, +) +``` + +Finally, plot the trajectories to show that they are identical in the two simulations. + +```{code-cell} +# Plot the resulting particle trajectories overlapped for both cases +advection_then_wind = xr.open_zarr("advection_then_wind.zarr") +wind_then_advection = xr.open_zarr("wind_then_advection.zarr") +plt.plot(wind_then_advection.lon.T, wind_then_advection.lat.T, "-") +plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, "--", c="k", alpha=0.7) +plt.show() +``` + +## Warning! Avoid updating particle locations directly in Kernels + +It is better not to update `particles.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particles.lon` in a Kernel will throw a warning. + +Instead, update the local variable `particles.dlon`. + +## Working with Status Codes + +In order to capture errors in the Kernel loop, Parcels uses a Status Code system. There are several Status Codes, listed below. + +```{code-cell} +from parcels import StatusCode + +for statuscode, val in StatusCode.__dict__.items(): + if statuscode.startswith("__"): + continue + print(f"{statuscode} = {val}") +``` + +Once an error is thrown (for example, a Field Interpolation error), then the `particles.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. + +For example, you can write a Kernel that checks for `particles.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`. + +``` +def CheckOutOfBounds(particles, fieldset): + if particles.state == StatusCode.ErrorOutOfBounds: + particles.delete() + + +def CheckError(particles, fieldset): + if particles.state >= 50: # This captures all Errors + particles.delete() +``` + +But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particles.state = StatusCode.Success` yourself. For example: + +``` +def Move1DegreeWest(particles, fieldset): + if particles.state == StatusCode.ErrorOutOfBounds: + particles.dlon -= 1.0 + particles.state = StatusCode.Success +``` + +Or, if you want to make sure that particles don't escape through the water surface + +``` +def KeepInOcean(particles, fieldset): + if particles.state == StatusCode.ErrorThroughSurface: + particles.dz = 0.0 + particles.state = StatusCode.Success +``` + +Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. + +Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particle_dlon` variables. diff --git a/docs/user_guide/examples/tutorial_kernelloop.ipynb b/docs/user_guide/examples/tutorial_kernelloop.ipynb deleted file mode 100644 index bc4b863d79..0000000000 --- a/docs/user_guide/examples/tutorial_kernelloop.ipynb +++ /dev/null @@ -1,594 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Parcels Kernel loop\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial explains how Parcels executes multiple Kernels, and what happens under the hood when you combine Kernels. \n", - "\n", - "This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle.\n", - "\n", - "In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Basic implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Below is a structured overview of the Kernel loop is implemented. Note that this is for `lon` only, but the same process is applied for `lat` and `z`.\n", - "\n", - "1. Initialise an extra Variable `particles.dlon=0`\n", - "\n", - "2. Within the Kernel loop, for each particle:
\n", - "\n", - " 1. Update `particles.lon += particles.dlon`
\n", - "\n", - " 2. Update `particles.time += particles.dt` (except for on the first iteration of the Kernel loop)
\n", - "\n", - " 3. Set variable `particles.dlon = 0`
\n", - "\n", - " 4. For each Kernel in the list of Kernels:\n", - " \n", - " 1. Execute the Kernel\n", - " \n", - " 2. Update `particles.dlon` by adding the change in longitude, if needed
\n", - "\n", - " 5. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file
\n", - "\n", - "Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particle.temp = fieldset.Temp[particle.time, particle.z, particle.lat, particle.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example use" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "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." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.8.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.2/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.8.0.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.8.0.min.js\", \"https://cdn.holoviz.org/panel/1.8.2/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "6ef4fa27-3f74-4b8e-9367-db5920d64218" - } - }, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const py_version = '3.8.0'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.2/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", - "application/vnd.holoviews_load.v0+json": "" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/fd/p7j_05zx409dzmtfjjjnjgfh0000gn/T/ipykernel_19079/1981051754.py:7: UserWarning: This is an alpha version of Parcels v4. The API is not stable and may change without deprecation warnings.\n", - " import parcels\n" - ] - } - ], - "source": [ - "from datetime import timedelta\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import xarray as xr\n", - "\n", - "import parcels" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: cf_xarray found variable 'uo' with CF standard name 'eastward_sea_water_velocity' in dataset, renamed it to 'U' for Parcels simulation.\n", - "INFO: cf_xarray found variable 'vo' with CF standard name 'northward_sea_water_velocity' in dataset, renamed it to 'V' for Parcels simulation.\n" - ] - } - ], - "source": [ - "# Load the CopernicusMarine data in the Agulhas region from the example_datasets\n", - "example_dataset_folder = parcels.download_example_dataset(\n", - " \"CopernicusMarine_data_for_Argo_tutorial\"\n", - ")\n", - "\n", - "ds_fields = xr.open_mfdataset(f\"{example_dataset_folder}/*.nc\", combine=\"by_coords\")\n", - "ds_fields.load() # load the dataset into memory\n", - "\n", - "# Create an idealised wind field and add it to the fieldset\n", - "tdim, ydim, xdim = (len(ds_fields.time),len(ds_fields.latitude), len(ds_fields.longitude))\n", - "ds_fields[\"UWind\"] = xr.DataArray(\n", - " data=np.ones((tdim, ydim, xdim)) * np.sin(ds_fields.latitude.values)[None, :, None],\n", - " coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])\n", - "\n", - "ds_fields[\"VWind\"] = xr.DataArray(\n", - " data=np.zeros((tdim, ydim, xdim)),\n", - " coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])\n", - "\n", - "fieldset = parcels.FieldSet.from_copernicusmarine(ds_fields)\n", - "\n", - "fieldset.UWind.units = parcels.GeographicPolar()\n", - "fieldset.VWind.units = parcels.Geographic()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now define a wind kernel that uses a forward Euler method to apply the wind forcing. Note that we update the `particle_dlon` and `particle_dlat` variables, rather than `particle.lon` and `particle.lat` directly." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def wind_kernel(particles, fieldset):\n", - " dt_float = particles.dt / np.timedelta64(1, 's')\n", - " particles.dlon += (\n", - " fieldset.UWind[particles] * dt_float\n", - " )\n", - " particles.dlat += (\n", - " fieldset.VWind[particles] * dt_float\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Run a simulation where we apply first kernels as `[AdvectionRK4, wind_kernel]`" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in /Users/Gebruiker/Documents/UU/parcels/Parcels/docs/user_guide/examples/advection_then_wind.zarr\n", - "Integration time: 2024-01-05T18:00:11.128799232: 100%|██████████| 432000.0/432000.0 [00:01<00:00, 368600.70it/s]\n" - ] - } - ], - "source": [ - "npart = 10\n", - "z = np.repeat(ds_fields.depth[0].values, npart)\n", - "lons = np.repeat(31, npart)\n", - "lats = np.linspace(-32.5, -30.5, npart)\n", - "\n", - "pset = parcels.ParticleSet(fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons)\n", - "output_file = parcels.ParticleFile(\n", - " store=\"advection_then_wind.zarr\", outputdt=timedelta(hours=6)\n", - ")\n", - "pset.execute(\n", - " [parcels.kernels.AdvectionRK4, wind_kernel],\n", - " runtime=timedelta(days=5),\n", - " dt=timedelta(hours=1),\n", - " output_file=output_file,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]`" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in /Users/Gebruiker/Documents/UU/parcels/Parcels/docs/user_guide/examples/wind_then_advection.zarr\n", - "Integration time: 2024-01-05T18:00:11.128799232: 100%|██████████| 432000.0/432000.0 [00:01<00:00, 331309.93it/s]\n" - ] - } - ], - "source": [ - "pset_reverse = parcels.ParticleSet(\n", - " fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons\n", - ")\n", - "output_file_reverse = parcels.ParticleFile(\n", - " store=\"wind_then_advection.zarr\", outputdt=timedelta(hours=6)\n", - ")\n", - "pset_reverse.execute(\n", - " [wind_kernel, parcels.kernels.AdvectionRK4],\n", - " runtime=timedelta(days=5),\n", - " dt=timedelta(hours=1),\n", - " output_file=output_file_reverse,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, plot the trajectories to show that they are identical in the two simulations." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Plot the resulting particle trajectories overlapped for both cases\n", - "advection_then_wind = xr.open_zarr(\"advection_then_wind.zarr\")\n", - "wind_then_advection = xr.open_zarr(\"wind_then_advection.zarr\")\n", - "plt.plot(wind_then_advection.lon.T, wind_then_advection.lat.T, \"-\")\n", - "plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, \"--\", c=\"k\", alpha=0.7)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Caveat: Avoid updating particle locations directly in Kernels" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is better not to update `particle.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particle.lon` in a Kernel will throw a warning. \n", - "\n", - "Instead, update the local variable `particle.dlon`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with Status Codes" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to capture errors in the Kernel loop, Parcels uses a Status Code system. There are several Status Codes, listed below." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Success = 0\n", - "EndofLoop = 1\n", - "Evaluate = 10\n", - "Repeat = 20\n", - "Delete = 30\n", - "StopExecution = 40\n", - "StopAllExecution = 41\n", - "Error = 50\n", - "ErrorInterpolation = 51\n", - "ErrorGridSearching = 52\n", - "ErrorOutOfBounds = 60\n", - "ErrorThroughSurface = 61\n", - "ErrorOutsideTimeInterval = 70\n" - ] - } - ], - "source": [ - "from parcels import StatusCode\n", - "\n", - "for statuscode, val in StatusCode.__dict__.items():\n", - " if statuscode.startswith(\"__\"):\n", - " continue\n", - " print(f\"{statuscode} = {val}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once an error is thrown (for example, a Field Interpolation error), then the `particle.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. \n", - "\n", - "For example, you can write a Kernel that checks for `particle.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "def CheckOutOfBounds(particles, fieldset):\n", - " if particles.state == StatusCode.ErrorOutOfBounds:\n", - " particles.delete()\n", - "\n", - "\n", - "def CheckError(particles, fieldset):\n", - " if particles.state >= 50: # This captures all Errors\n", - " particles.delete()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particle.state = StatusCode.Success` yourself. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "def Move1DegreeWest(particles, fieldset):\n", - " if particles.state == StatusCode.ErrorOutOfBounds:\n", - " particles.dlon -= 1.0\n", - " particles.state = StatusCode.Success" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or, if you want to make sure that particles don't escape through the water surface" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "def KeepInOcean(particles, fieldset):\n", - " if particles.state == StatusCode.ErrorThroughSurface:\n", - " particles.dz = 0.0\n", - " particles.state = StatusCode.Success" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. \n", - "\n", - "Note that these Kernels that control what to do with `particle.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particle.state` or the `particle_dlon` variables." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "test-notebooks", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.0" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} From 83b4b3fa3f4ea02c22a0224aa183d503cafb8cc4 Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Thu, 20 Nov 2025 16:05:33 +0100 Subject: [PATCH 3/9] fix statuscodes boolean indexing and wording --- .../examples/explanation_kernelloop.md | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index e445e961f3..fcbbb43fb2 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -6,7 +6,7 @@ kernelspec: # The Parcels Kernel loop -This tutorial explains how Parcels executes multiple Kernels, and what happens under the hood when you combine Kernels. +On this page we discuss Parcels' execution loop, and what happens under the hood when you combine multiple Kernels. This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels! @@ -18,7 +18,7 @@ In order to make sure that the displacements of a particle in the different Kern ## Basic implementation -Below is a structured overview of the Kernel loop is implemented. Note that this is for `lon` only, but the same process is applied for `lat` and `z`. +Below is a structured overview of how the Kernel loop is implemented. Note that this is for `time` and `lon` only, but the process for `lon` is also applied to `lat` and `z`. 1. Initialise an extra Variable `particles.dlon=0` @@ -38,9 +38,9 @@ Below is a structured overview of the Kernel loop is implemented. Note that this Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particles.temp = fieldset.Temp[particles.time, particles.z, particles.lat, particles.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file. -## Example with multiple Kernels +## Example with currents and winds -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, as we will show. +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. ```{code-cell} import matplotlib.pyplot as plt @@ -87,7 +87,7 @@ def wind_kernel(particles, fieldset): ) ``` -Run a simulation where we apply first kernels as `[AdvectionRK4, wind_kernel]` +First run a simulation where we apply kernels as `[AdvectionRK4, wind_kernel]` ```{code-cell} :tags: [hide-output] @@ -98,29 +98,30 @@ lats = np.linspace(-32.5, -30.5, npart) pset = parcels.ParticleSet(fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons) output_file = parcels.ParticleFile( - store="advection_then_wind.zarr", outputdt=timedelta(hours=6) + store="advection_then_wind.zarr", outputdt=np.timedelta64(6,'h') ) pset.execute( [parcels.kernels.AdvectionRK4, wind_kernel], - runtime=timedelta(days=5), - dt=timedelta(hours=1), + runtime=np.timedelta64(5,'D'), + dt=np.timedelta64(1,'h'), output_file=output_file, ) ``` -And also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]` +Then also run a simulation where we apply the kernels in the reverse order as `[wind_kernel, AdvectionRK4]` ```{code-cell} +:tags: [hide-output] pset_reverse = parcels.ParticleSet( fieldset, pclass=parcels.Particle, z=z, lat=lats, lon=lons ) output_file_reverse = parcels.ParticleFile( - store="wind_then_advection.zarr", outputdt=timedelta(hours=6) + store="wind_then_advection.zarr", outputdt=np.timedelta64(6,"h") ) pset_reverse.execute( [wind_kernel, parcels.kernels.AdvectionRK4], - runtime=timedelta(days=5), - dt=timedelta(hours=1), + runtime=np.timedelta64(5,"D"), + dt=np.timedelta64(1,"h"), output_file=output_file_reverse, ) ``` @@ -160,34 +161,34 @@ Once an error is thrown (for example, a Field Interpolation error), then the `pa For example, you can write a Kernel that checks for `particles.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`. ``` -def CheckOutOfBounds(particles, fieldset): - if particles.state == StatusCode.ErrorOutOfBounds: - particles.delete() +def DeleteOutOfBounds(particles, fieldset): + out_of_bounds = particles.state == StatusCode.ErrorOutOfBounds + particles[out_of_bounds].state = StatusCode.Delete -def CheckError(particles, fieldset): - if particles.state >= 50: # This captures all Errors - particles.delete() +def DeleteAnyError(particles, fieldset): + any_error = particles.state >= 50 # This captures all Errors + particles[any_error].state = StatusCode.Delete ``` But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particles.state = StatusCode.Success` yourself. For example: ``` def Move1DegreeWest(particles, fieldset): - if particles.state == StatusCode.ErrorOutOfBounds: - particles.dlon -= 1.0 - particles.state = StatusCode.Success + out_of_bounds = particles.state == StatusCode.ErrorOutOfBounds + particles[out_of_bounds].dlon -= 1.0 + particles[out_of_bounds].state = StatusCode.Success ``` Or, if you want to make sure that particles don't escape through the water surface ``` def KeepInOcean(particles, fieldset): - if particles.state == StatusCode.ErrorThroughSurface: - particles.dz = 0.0 - particles.state = StatusCode.Success + through_surface = particles.state == StatusCode.ErrorThroughSurface + particles[through_surface].dz = 0.0 + particles[through_surface].state = StatusCode.Success ``` Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. -Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particle_dlon` variables. +Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particle.dlon` variables. From 4647f10ddc9437533ebe4c14f4d2be2bbc2bdba8 Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Fri, 21 Nov 2025 16:54:39 +0100 Subject: [PATCH 4/9] add kernel loop explanation to user guide --- docs/user_guide/examples/explanation_kernelloop.md | 2 +- docs/user_guide/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index fcbbb43fb2..d72e510ee4 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -191,4 +191,4 @@ def KeepInOcean(particles, fieldset): Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. -Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particle.dlon` variables. +Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particles.dlon` variables. diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index e1ee35f0bf..fc787c9d9d 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -44,6 +44,7 @@ examples/tutorial_delaystart.ipynb :caption: Write Kernels :titlesonly: +examples/kernelloop_explanation.md examples/tutorial_sampling.ipynb examples/tutorial_gsw_density.ipynb examples/tutorial_Argofloats.ipynb From 4059bb9fc99730c01e531a4322d9ec7324b29623 Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Fri, 21 Nov 2025 16:58:11 +0100 Subject: [PATCH 5/9] fix typo --- docs/user_guide/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index fc787c9d9d..8f37d6e388 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -44,7 +44,7 @@ examples/tutorial_delaystart.ipynb :caption: Write Kernels :titlesonly: -examples/kernelloop_explanation.md +examples/explanation_kernelloop.md examples/tutorial_sampling.ipynb examples/tutorial_gsw_density.ipynb examples/tutorial_Argofloats.ipynb From 178e128e33520198b4f3a64b4a3c6fe1a8efff0d Mon Sep 17 00:00:00 2001 From: Reint Date: Mon, 24 Nov 2025 13:28:23 +0100 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com> Co-authored-by: Erik van Sebille --- docs/user_guide/examples/explanation_kernelloop.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index d72e510ee4..eeacf9c465 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -8,13 +8,13 @@ kernelspec: On this page we discuss Parcels' execution loop, and what happens under the hood when you combine multiple Kernels. -This is probably not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels! +This is not very relevant when you only use the built-in Advection kernels, but can be important when you are writing and combining your own Kernels! ## Background -When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle. +When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through time and executes the Kernels for all particle. -In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement. +In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.dz`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by summing the _changes_ in position, the ordering of the Kernels has no consequence on the particle displacement. ## Basic implementation @@ -158,7 +158,7 @@ for statuscode, val in StatusCode.__dict__.items(): Once an error is thrown (for example, a Field Interpolation error), then the `particles.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. -For example, you can write a Kernel that checks for `particles.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this to the Kernel list in `pset.execute()`. +For example, you can write a Kernel that checks for `particles.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this custom Kernel to the Kernel list in `pset.execute()`. ``` def DeleteOutOfBounds(particles, fieldset): From 8fcfbde0dc74be2fa73a0c63043cfc40ff1d1943 Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Mon, 24 Nov 2025 17:24:03 +0100 Subject: [PATCH 7/9] add formatting suggestions from review --- .../examples/explanation_kernelloop.md | 54 +------------------ docs/user_guide/index.md | 1 + 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index eeacf9c465..e834b73bee 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -137,58 +137,8 @@ plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, "--", c="k", alph plt.show() ``` -## Warning! Avoid updating particle locations directly in Kernels - +```{warning} It is better not to update `particles.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particles.lon` in a Kernel will throw a warning. Instead, update the local variable `particles.dlon`. - -## Working with Status Codes - -In order to capture errors in the Kernel loop, Parcels uses a Status Code system. There are several Status Codes, listed below. - -```{code-cell} -from parcels import StatusCode - -for statuscode, val in StatusCode.__dict__.items(): - if statuscode.startswith("__"): - continue - print(f"{statuscode} = {val}") -``` - -Once an error is thrown (for example, a Field Interpolation error), then the `particles.state` is updated to the corresponding status code. This gives you the flexibility to write a Kernel that checks for a status code and does something with it. - -For example, you can write a Kernel that checks for `particles.state == StatusCode.ErrorOutOfBounds` and deletes the particle, and then append this custom Kernel to the Kernel list in `pset.execute()`. - -``` -def DeleteOutOfBounds(particles, fieldset): - out_of_bounds = particles.state == StatusCode.ErrorOutOfBounds - particles[out_of_bounds].state = StatusCode.Delete - - -def DeleteAnyError(particles, fieldset): - any_error = particles.state >= 50 # This captures all Errors - particles[any_error].state = StatusCode.Delete -``` - -But of course, you can also write code for more sophisticated behaviour than just deleting the particle. It's up to you! Note that if you don't delete the particle, you will have to update the `particles.state = StatusCode.Success` yourself. For example: - -``` -def Move1DegreeWest(particles, fieldset): - out_of_bounds = particles.state == StatusCode.ErrorOutOfBounds - particles[out_of_bounds].dlon -= 1.0 - particles[out_of_bounds].state = StatusCode.Success -``` - -Or, if you want to make sure that particles don't escape through the water surface - -``` -def KeepInOcean(particles, fieldset): - through_surface = particles.state == StatusCode.ErrorThroughSurface - particles[through_surface].dz = 0.0 - particles[through_surface].state = StatusCode.Success -``` - -Kernel functions such as the ones above can then be added to the list of kernels in `pset.execute()`. - -Note that these Kernels that control what to do with `particles.state` should typically be added at the _end_ of the Kernel list, because otherwise later Kernels may overwrite the `particles.state` or the `particles.dlon` variables. +``` \ No newline at end of file diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 8f37d6e388..a8cd3dfb0a 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -45,6 +45,7 @@ examples/tutorial_delaystart.ipynb :titlesonly: examples/explanation_kernelloop.md +examples/tutorial_statuscodes.md examples/tutorial_sampling.ipynb examples/tutorial_gsw_density.ipynb examples/tutorial_Argofloats.ipynb From 310a133eb3ac01989fccc3d957509243feb05f9d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:25:13 +0000 Subject: [PATCH 8/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/user_guide/examples/explanation_kernelloop.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index e834b73bee..26f7607dd2 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -137,8 +137,8 @@ plt.plot(advection_then_wind.lon.T, advection_then_wind.lat.T, "--", c="k", alph plt.show() ``` -```{warning} +```{warning} It is better not to update `particles.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particles.lon` in a Kernel will throw a warning. Instead, update the local variable `particles.dlon`. -``` \ No newline at end of file +``` From 944ad8c76a88b33fac67844e22273a25b41a4e5d Mon Sep 17 00:00:00 2001 From: reint-fischer Date: Mon, 24 Nov 2025 17:29:22 +0100 Subject: [PATCH 9/9] remove statuscodes from toctree --- docs/user_guide/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index a8cd3dfb0a..8f37d6e388 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -45,7 +45,6 @@ examples/tutorial_delaystart.ipynb :titlesonly: examples/explanation_kernelloop.md -examples/tutorial_statuscodes.md examples/tutorial_sampling.ipynb examples/tutorial_gsw_density.ipynb examples/tutorial_Argofloats.ipynb