From 374825b1b441006456868fec7ce7419c7c1a29b4 Mon Sep 17 00:00:00 2001 From: ChitYanToe Date: Wed, 22 Jul 2026 10:30:49 +0200 Subject: [PATCH 01/15] Create SWASH notebook --- docs/user_guide/examples/tutorial_swash.ipynb | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/user_guide/examples/tutorial_swash.ipynb diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb new file mode 100644 index 000000000..764c2d1de --- /dev/null +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -0,0 +1,25 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "da6f7651", + "metadata": {}, + "source": [ + "# Tutorial for using SWASH data in Parcels" + ] + }, + { + "cell_type": "markdown", + "id": "6f087323", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 0364fd7a1c04ade7c4a811566fefe78b44d11171 Mon Sep 17 00:00:00 2001 From: ChitYanToe Date: Wed, 22 Jul 2026 15:52:44 +0200 Subject: [PATCH 02/15] swash_to_sgrid converter Added the converter function to convert SWASH output files to sgrid data --- docs/user_guide/examples/tutorial_swash.ipynb | 316 +++++++++++++++++- src/parcels/convert.py | 124 +++++++ src/parcels/tutorial.py | 4 +- 3 files changed, 440 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index 764c2d1de..9a19f33d9 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -8,16 +8,328 @@ "# Tutorial for using SWASH data in Parcels" ] }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cf9f8ca5", + "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 version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\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, Bokeh, 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 const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\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.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/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 } 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 = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\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, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\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": "48eaae67-aca7-4ad9-8591-d742d6cc18b2" + } + }, + "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 version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\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, Bokeh, 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 const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\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.9.3/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 } 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 = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\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, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\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/2v/5l455kn13xq41s2x_zl1chlr0000gn/T/ipykernel_82001/1470123736.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": [ + "import matplotlib.pyplot as plt\n", + "import matplotlib.tri as mtri\n", + "import numpy as np\n", + "import uxarray as ux\n", + "import xarray as xr\n", + "\n", + "import parcels\n", + "import parcels.tutorial\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1a044f75", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/YanTo001/Library/Caches/parcels/data-matlab/F1GRD.mat\n" + ] + } + ], + "source": [ + "mat_files = parcels.tutorial.open_dataset(\n", + " \"SWASH_data/data\", download_only=True\n", + ")\n", + "print(mat_files)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "81c491ae", + "metadata": {}, + "outputs": [], + "source": [ + "grid_file = \"/Users/YanTo001/Documents/GitHub/parcels-data/data-matlab/F1GRD.mat\"#\"/Users/YanTo001/Library/Caches/parcels/data-matlab/F1GRD.mat\"\n", + "result_file = \"/Users/YanTo001/Documents/GitHub/parcels-data/data-matlab/F1ALL.mat\"#\"/Users/YanTo001/Library/Caches/parcels/data-matlab/F1ALL.mat\"\n", + "ds = parcels.convert.swash_to_sgrid(grid_file, result_file, total_depth=8.0)\n", + "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "7fd5d3d1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in output-quickstart.parquet\n", + "\n", + "\u001b[A\n", + "\u001b[A\n", + "\u001b[A\n", + "\u001b[A\n", + "\u001b[A\n", + "Integration time: 2026-06-01T00:00:15 100%|██████████| [00:00<00:00, 1151.08it/s]\n" + ] + } + ], + "source": [ + "npart = 150 # number of particles to be released\n", + "# release particles in a line along a meridian\n", + "lat = np.linspace(1, 16, npart) ## y\n", + "lon = np.repeat(15, npart) ## x\n", + "time = np.repeat(ds.time.values[0], npart)\n", + "layerI = 0 # at which water depth, the particles are released\n", + "z = np.repeat(ds.depth.values[layerI], npart)\n", + "pset = parcels.ParticleSet(fieldset=fieldset, pclass=parcels.Particle, t=time, x=lon, y=lat, z=z)\n", + "kernels = [parcels.kernels.AdvectionRK2]\n", + "\n", + "output_file = parcels.ParticleFile(\"output-quickstart.parquet\", outputdt=np.timedelta64(5, \"s\"), mode=\"w\") # \n", + "pset.execute(\n", + " kernels,\n", + " runtime=np.timedelta64(20, \"s\"),\n", + " dt=np.timedelta64(5, \"s\"),\n", + " output_file=output_file,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "7933d20b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAHGCAYAAABU9964AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAbv5JREFUeJzt3Qd8G/X5P/DPadqWR+wMErJDwsggJIGwEnYZYe89yujgB2UU/pCWUaBtCqWFsmdpyyi0lBn2SFghrAQII0ASsoczvWTNu//r+d6dLHnFks62JH/evA5Z57vTSXasR8/3ueerGYZhgIiIiIjS4kpvcyIiIiJiEEVERESUIWaiiIiIiDLAIIqIiIgoAwyiiIiIiDLAIIqIiIgoAwyiiIiIiDLAIIqIiIgoAwyiqFP9/e9/x2uvvcZXmYiICo7GjuWUrQcffBBDhw7FwQcf3OJ7w4YNw5QpU/DYY48V7Au9Zs0afPLJJ1i3bp16Hfbcc0+UlZW1uf3nn3+OTz/9FB6PB/vssw9GjBjhyHFjsRjee+89/PDDD+jfvz/233//drfP5ty64/G+//57vPnmm4hEIrj44ovhdrsT3/vggw/Ua7U1Pp8PF154YUaPn862ThzDicdr7zXrjMcj6mmYiaKsXXvttXjiiSda/d65556LQw89tCBf5U2bNuGoo47CkCFDcPvtt6s38SuuuALbbbcdHn/88Rbbx+NxnHnmmSqofPfdd/HSSy9h9OjRuPHGG7M6rvjwww+x4447qjfKzz77DE8++SQmTZqEWbNmdei5dPTcuuPxnn76aUyYMAFHHnkk/vKXv+Cyyy5DNBpN2Wbz5s1YunRpYvniiy/Udo8++mjK+mXLlqX9+Om+Ntk+XyceryOvmZOPR9RjSSaKKBvbbLONcfbZZ/e4F/Hbb781hg4danzwwQeJdfF43DjnnHMMl8tlzJ8/P2X73//+94bb7TY++uijxLrHH39c5q40nn322YyP+9133xmBQMCYPn26oet6Yv369euNzz77rEPPpaPn1h2P9+qrrxrz5s1TX59++ulqm8bGxnaP/8MPP6jtfv3rX2f9+Ols68TzdeLx0nnNnHg8op6KQRRlLBaLGbfddptRVlZm7LbbbuprWZ588snENg8//LD6g57snnvuMd566y31tQQK999/v/Huu++mbDNnzhzjvvvuM15++eWUN+pksl62e+CBB4wHH3zQ+PLLL7v0p7lhwwZj9erVLdZLICFvQNdff33Ka9WvXz/jsMMOa7H9iBEjjKlTp2Z0XHHUUUcZo0aNUoFWJtI5t+54vGROBFHpPH665yqBiPwbWL58ebc9XjqvWTY/CyIyDE93Z8Iof0kQLkMkMhxQX1+vvm5OhgRkmOCQQw5JrJs+fTpOOeUUNQQo9T7l5eW45JJL1NDfnXfeiZ/+9KfYsGEDKisr8cwzz6jhQLlNtnz5cpxwwglYsmQJDjroILVOhiyOPfZYVcwudR1tefHFF7F48eK062ea6927d6vra2tr1a2u64l133zzDaqrq9Vr0dzUqVPVMF04HIbf70/ruHV1dWr45dJLL8X69evx1ltvoaamBqNGjVI1Su3VwGRybl39eJ0hncdP91xff/11Nbw9duxYDB48uFser7NeCyJqiUEUZUwCFanZkXqYyZMnq6876oUXXsAdd9yhAiHxr3/9C2effbb6gy1B06mnnqrWS6B1+umn45133sG+++6r1knQdsQRR6ht5U2gX79+av1XX32FXXfdFTvttJMK1Nry8MMP4/nnn9/qOQYCgXaDqLZIDYo4/PDDE+t+/PFHdTto0KAW28ubnxRpS2AowUg6x/3666/V6zFv3jyMHDkSBxxwgCru/u1vf4uBAweqgFGK+9uTzrl19eN1hnQeP91z3X333dUHAqln667H66zXgohaYhBF3WKbbbZJBFDipJNOUkHURx99hIceeiixXrY544wzVMGyHUS98sorWLBggQre7ABKyKdxOc4DDzzQbhAlxbZbe6MXmXz6/tvf/oaZM2firLPOUm9wtoaGBnVbXFzcYh97nb1NOsfdsmWLupXX59lnn8Uxxxyj7kuGbpdddsFpp52GOXPmJF637777LrFv3759VYCazrl19eN1hnQeP91z/clPfqKW7ny8dHT3z4Io3zGIom4xZsyYlPtFRUWoqKhosV6G1KqqqrBq1arEOrkaTEhW5K677lLDirIIGWKSYcX2hiDOO++8TnhGwFNPPYXLL79cDY3ce++9rb4hyXk1Z69r7Y1sa8eV103I1VR2QCPk8nQJPmV7CXDk/j//+U91LNv48eNVUJPOuXX143WGdB7fiXPt6sdLR3f/LIjyHYMo6halpaWtDg+2tT758mz767Vr1ybqhGw77LCD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YCaKiIgoX7CwPKcwiCIiIsoXDKJySrcO54XDYTz22GOYMmUK+vTpgw8++KDFNsOGDVPfa75cddVVbR73iiuuaLH91KlTkTPD2QU0HC0N+7qqaZ9UAeg98DUmImoRRGW7UP5noq699lqsWrUKF198MU455RREo2Y37mSfffaZmtzR9uGHH+Koo47CgQce2OZx6+vrsddee+Hvf/97Yp3Hw6QbEREROadbI4ubb75ZtclfuXJlm9v07t075f5zzz2nslMHHXTQVmeolgwUERFRwTAcyCTJMSj/h/PSnWdIMkz/+c9/cN5558Hlav/UZ82ahSFDhmDcuHG48MILsX79+izPloiIqHtphu7IQj2wxcFTTz2FxsZG/PSnP213uwEDBuDWW2/F7Nmzcc8992D+/PnYe++9EQwG263Pqq2tTVmoYxOJZjsRKhERUT7Kq0Khhx9+GNOmTcPAgQPb3e76669PfD1ixAg8//zzGDx4MJ588kmce+65re4zY8YM3HDDDY6fMxERkWN4dV5OyZtM1LfffquKyi+44IK09+3Xr58a2lu4cGGb20yfPh01NTWJZcWKFVmeMRERkcPsy4+zXahnZaIeeughlYGSTFS6ZBhv9erV6Nu3b5vb+P1+tXQ2TvtCRERUGPIiEyWtDx599FE1FOd2u1t8/7LLLkv0gZLaJslWLVmyRN2XgvJzzjlHtTg49dRTu/zciYiIHMM+UTmlW4MoqVGSNgQ777yzun/00Uer+7fcckvKdi+88AI2btzYZj1TXV0dNm/erL6WbJI07zziiCMQCARUOwQZnnvvvfcwaNCgLnhWREREnYRBVE7p1uG8Y489ttV+TyUlJSn3DzvsMJVRqqqqavU4t99+O2KxWOL+2WefrZZQKISioqJOOHMiIiLq6bo1iOpoHZIEVc0Dq2SlpaWtri/kACrNFluOpyCzfXyiQsfWH9QpZAaPbPs8Jc0CQj2ksJyIiKjHY4uDnMIgioiIKF840aKALQ561tV5RERERLmGmag8le2Qtu7Q4+uG88VRGjheT0TUKg7n5RQGUURERPlCisolkMr2GOQIDucRERERZYCZqC5m1/O5te5pMWDkULTND0NEROnRdF0t2ch2f2rCIIqIiCiv+kRlWTfKPlGO4XAeERERUQaYiSIiIsoXvDovpzCIylNtZmO3klvUMkxBtjWCzqkt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2pSFiIja7ljePBNl9HXhvcO2w1e79ld9okrqoogEemDaowuC82yXniwajapyIolVJMGybt06XHDBBYXX4sAu+orFYi1eALfb7dg+YsaMGbjhhhscOGsiosIXrvM33amzqmzW65j6ymK1SjJRDWVeeBt69hu20wwHaprMCrae54MPPsBjjz2G//73v2hoaMCRRx6phvUOO+wweL3ergui5DJBqTt6//33sXnz5hbfnz17NpwwaNAgdSttCpLJ/YEDBzq2j5g+fTouv/zyxH3JRA0ePDir8yciKlTekmjTnXKrJqqfC+9NHo6qdUGM/WQNSusiiDITRTlAWh7ts88+6qo8ufjs+OOPR3l5edbHzSiI+tnPfqbaERxzzDEYM2YMOktZWRkmTpyI1157DSeffHIioyRX811yySWJ7TZu3KiG47bddtsO79Na5b4sRES0dZEtSTVRtWZuxKjWsc/MpkxUfbkPPl6d5ygWlmfG5/NhxYoVKk5wUkZB1PPPP6/aB2y//fZZPXh9fT02bNiQyBrJrTTd7NWrl1qEtCeQ/k6TJk3Cnnvuib/85S9qWO6Xv/xl4jhXXXUV5s6di6+++qrD+xARUea8gaSSiTIrEyU1UXsNR8WaRoz7eDVKayKIlLDZsZOcqGnqiTVRLpcrEUAFg0F1AdqOO+6Y/XEz2amkpEQVamfr1VdfVc01pbhL+kVdffXV6v4//vGPxDZHH320GrN86qmnVK8nyTi9++67KY8vXycP1XVkHyIiylx4Sxs1Uc8vxs4fr4auMlF++II5MTEGEWpqatQIVWlpKXbZZZfEKyKjanYSpksyUdJPQYqw//rXv7ZbrL01ki3qSBdxGbuUpS1/+tOf0t6HiIgy50nORCXVRL2/53CUrw5inNRE1YYRYZ8oR0lReLaF4T21sPyKK65Qddwffvgh9t1338T6s846CzfeeCP+85//dE0QdfHFF2PnnXdWjaokg9S8uaYM9RERUeGK1LReEzXl+aSaqDI/fLw6z1GcgDhzzz33nOpQ3rwF0h577IGf//znGR0zoyDqpz/9KSorK3HiiScmapeIiKjn8FVEWmai+rrw/t4jULE6iLFSE1UfZsdyygnSUFOyUH379lWTETfvONB8XacGUdLaQOahGzFiREYPSkRE+S1a722ZiVqvY8pzi1IzUfU9c+ios7CwPPPCcpne5ZVXXlF9oZLJfL+777571wVRMhOyE/0ViIgoPxVVhprulGtWTZQb7+41HJV2JkpqotgnylGsicrcH//4R3Uh27nnnot4PK7qqV9++WU1/ct7772X0TEzumxChvF++9vfIhJJSucSEVGPEQ0mZ6KsUuXqOPZ5bpFqb6Cuzqvww89MVKfURGW79ETTpk3DSy+9hEWLFqlhvTvuuAMVFRVqdG3y5Mldl4mSOWcWLlyoJhqWrt7NC8szvVSQiIjyg7+q9UzU7D1GoGptPcZ9vAYBXp1HOWbq1KlqcUpGQVSmVexERFQY4iF3i0yUUR3Hfi/80DR3XrlkotgnqlCG86T4unnSxIl9MjlursgoiLr00kudPxMiIsob/l6hVvpEuTF78nD0XteQ6BMVZZ+ovG9xsHjxYpU8kabVMn2K1BXJUFhJSUlW+2Ry3HRIo+0ddtihQ9vKtG/fffdd5wVR9913H37xi184vi0REeWfeNjdSp+oOPabmXx1no9X5+U5qX2Wq9nGjRuHNWvWqKnaDj/8cFx44YUps4uku08mx02Xx+PBNddc0+FtM3qMjm4o8851NDBKZ1siIso/3vJoK32i3Hhnt2HoU92AMZ+uRWmtzJ3XQ6uYC6TFwcyZM1Uh9uzZs1WTSll+97vf4ZxzzsGf//xnVaCdyT6ZHDddMqPK+eefr76Wq/HuvPPOVkfS7O9lIq3Qa8qUKRk9CBERFRY97EKijMXKRGF9HPu+nNSxvNwHPzuWO0pC0mzD0nT2lylSRo0alZi8V+y///4q8Pjkk0/UFW+Z7JPJcbMRjUbV/LytBVHyvenTp2dUqtThIOr666/v8EEPOuigtE+EiIjyh6c4BmnyrAIpOxPVx40Pdh2KquoGjJ63DqV1EfaJymG1tbUt6oJkSbZu3boWWSH7/rp161o9bkf2yeS4nUVqs5pPBeN4ECVpNiIiIhELeZoyUXVWJmpDHFNeXdKUiSr1ce48h8mVddkO59lX50mLoubJktbe62XKlJT9rSlStHauqOvIPpkcN11SezV69Gh1bPl65MiRKd+XzJfUZJ133nkZHT+zSioiIurR3D69KRNV2pSJ+nDCEFRuCGKn+etQ2sBMlNMk7NAdOIZYsWJFyuwjzbNQYsCAAZgzZ07Kuurq6sTsJa3pyD6ZHDfTuqgrrrgCsVgMl112mfo6mdfrxfDhw9VQYiYYRBERUdoi9UlvH/VNmai93vixqU9UwAdvY372/+kJJIDa2hRuUgt9yy23YPny5RgyZIha99Zbb6mr2SYndfkOhUKqTYHMUdeRfTp6XCeCKLnQTbJeAwcOxLHHHgsnsQsaERFllIlKCJgdy43eLnx0wFAsHN8Pbt1ASUOUV+c5zDA0R5aOstsQXHDBBViyZAk+/vhj1TZA7ldVValtpD1BcXExnnjiiQ7v05FtnCTBndMBVMaZqFtvvbVFSqwj3yMiosIQrU0a+mmwqmw26tj97WUqoJJMVLDUCw8zUY5ycjivIyQzJJP0XnLJJdh1111VsCRNMWfMmJHYRmqYZChQsj4d3acj23SGBQsW4IUXXlBDmTLElzysd++993ZNEHXllVe2GSi19z0iIioMmldvKiwPmF8YlS7M23kgyjeFMGrBepWJigayfcun7u5YPmjQIPzvf/9r8/u9e/dWw3np7NPRbZz03//+F2effbaaO+/NN9/EEUccgXnz5mHlypU47rjjur8mKpvLBImIKH9EavxNheVB66qqzTomvbMikYlqDHjhCbFqhHLDddddh8cee0z1oOrVqxeef/55dcWeNOQsKyvr/CAq+dLA5pcJStHW6tWrcfrpp2d0IkRElD/cvnhTJqqkKRP11ej+KNkcxnbfbESRZKKKmYkqlAmI81k8Hle1V0ceeaQafpQGm7JOiuFvuukmVeh+9913d24QZQ/TybQurV0mOGzYsIwvEyQiovwRrfO1moka98FqlYnSXUCoxAtvuOe9YRfacF4hiEajKniSWEXIqJmMnm2//fYqmGpsbMzouGkFUfZ8eH369MEJJ5yQ0QMSEVH+c/lbyURVaFi40zYo2hzB8O82oSgomage+I5NOU8yUlIfdfLJJ+M///mPqpPKREaD1QygiIh6NjsTpdiZqBoDO81di2HfbYKuAaFiL9whZqI6Yzgv26Wn8fl8ePbZZxP3//rXv6oWC//4xz9Us81MrszLqrD8m2++wXPPPacaZSVfJigeeuihTA9LRER5wBuQ4ZFmmahyDT/s0A/+zVEMXbQJ/lAMsSJmopzE4bzMyFDevvvum7hfUVGBBx54ANnKKIiSHguSApMTeu2113D00UerywSl74LTMy8TEVHuiYVcTTVRjVYmqtbA9p+sM2uiNCDs98AT6XlZD8o94XBYXZG32267Yb/99lPL3nvvjZKSkq4PomSSwocffhinnXaaiu4kIyVFWxdeeGGLCQWJiKjw+MqSMlHFZmWIUaZh0ci+KK6JYNCSLfBHYoj5mYlyEjNRmSkqKsKsWbMwe/Zstdx+++0qbrGDKrko7qCDDuqamqiFCxeq7JPddVSq2qXi/fe//73KUhERUWGLNXqaaqIarQ/PdQZGza/GwCVbEpkod6Q7z7LwsCYqc5J5+u1vf4s33ngDW7ZsUUGV1EPdfPPNqvFml2WipDNpIBBIzMT8ww8/YOedd87qMkEiIsof3rJIUybKb31RqmHx8N4oro1i22U18IdjiJtXlBPlhO+//z6RkZJbGeaTefwyyUI50rFcLhM877zzVJPNZ555JuPLBImIKH/Ew+6mmqiwlZKqN7Ddgg0qCyVLxOeGO8qaKCfJa55tn6dEBrEHiUQiqpdlMBhUcYoM4Um/ywkTJqjJiTOV0Z4vvvhi4us//elPavJAuUxw22235ZV5REQ9gDcQa8pEFVlfBDT8uFNvrB9YBs0AfJE4dE8PfMfuggmIs116Gk3T1AiajKTV1tairq4O9fX1qi4qGxllopLHDmW+mUz7KxARUX6KJWeiQlag1GBg2LcbYWhQi2SiXMxEOcowNLVke4yexuv1qtIjmZ7OHs6TOfNk8uE999wTBx54oKqXShdnhiQiorR5kjuW+6wvijWsHFWJjf1LVSbKG2UminKLjJhJ+dFtt92GO++8UzUPf/fdd9X8eZ2aiZKxxI5aunRpRidDRFQoXBJFFLBYMCkTFbGea6OBgYs2w5CVGhDzuOCK9bysR2dyYjiuJw7nGYahrsqzC8o//fRT+P1+lYW64YYbcMABB3RuEHXNNddk9ABERFR4XB6jZSaqSMPaAeXwN0RRVR2EJ26wJsph7BOVGbkK77jjjlNBk5Qk3XrrrZg8eXJiQuJMdTiIkrFDIiIiEQt6Wmaiwga2WV6rOparTJTbBXecmSjKjWabmzdvzjpoao41UURElLaUSWztTJQP2LRtKep6FckG8MR16G6+uE4yHFp6Iq/DAZQjfaKIiKjniTUkvX1ErbflCFC5tiHxLh2T/jvx7jm/wh7Oyy67l22fKWrCTBQREWUg6Y3cY33tBer6FKGhzGeulrlUmYmiAsZMFBERpS0WTnr7iNupJyCwKWQ2iVKZKGldzhfXSU4MxzER1YOCqPHjx6OhoaHFeunzIJcltua6667DE088kbJu++23x8svv9xp50lE1JOkDCi5rXtuIFThgxbS4Y3E4TYMGG6+ZTuJV+fllpwPop599lnokhK2zJs3DyeffDJ22223Nveprq7GqFGjVCMtm/SDICIiZ8RD7pZFNnHAXx9N1EHp0KDJJHpEBSrng6gRI0ak3P/LX/6CgQMHqlmX2yNz5IwcObKTz46IqGfylkea+kTZty4gWuSBFjHgC8fhkkxUgTcd7Wpstplb8qqwvLGxEf/+979x7rnnwu1uv1rx/fffx84776xma/7Nb36jJhskIiJnaDBUn6gUBuCO6HDHzdEDXdOg9cB52jqTvOZOLNRDMlHJnn76aRUMnXfeee1uV1VVhauuugr77rsvVq1apbqtz5w5U7V59/nMq0Za62Yqi01meSYi6uppYrK9fL2rFPdPqlVNelPW3TKEZ36dH88k//pzyTBptsegHhhEPfzww/jJT36CoUOHtrudTCRoZ6omTpyICRMmYPjw4XjyySdx1llntbrPjBkz2ixUJyKiVJqnlcvuNPMNWrOH8NSlZEx7UOHKm+G8RYsWqZmWL7jggq1u23yob9CgQWoC5a+//rrNfaZPn46amprEsmLFCkfOm4ioEJUNaSVbrwOuuA6X3fJA5iHOk8xavuBwXm7Jm0zU3//+d/Tt2xdHHXVU2vvKMN3atWvRq1evNreRq/d4BR8RUcd4SqKtFpZHfG54reZQmmGAdeXOYmF5bsmLTFQ8Hsc///lPnHPOOa3OfSOF49OmTVNfRyIRXHnlldi4caO6HwwGceGFF6pjSGsEIiLKjubSVRCVGKmzb3XAH4rBEzV7HOiupvoookKUF5koaZK5evVqnH/++W32hVq+fLn6WoIsGb6TK/Okv5TM2iwNO996660W7RKIiCh93rIIXMlVE1YmynBrCJb6VHsD1eKAk7Q5js02c0teBFF77703lixZoorD2yoKD4VC6mtN03DJJZeoZd26daioqEBRUVEXnzERUeHylEZUFioxnGdfjRczUFYTTjTflEyUK8aaKCdx2pfckhdBlLQskKUtUivVmm222aYTz4qIqG12qwK7dYFTnD5eJkoH1TUFUEnTvsS9LmypKlZDet7NITMTxRiKClheBFFERJRLmgVyMfO+O6qjckNQfVvW6G4XM1EO43Be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LlaPLedQXl6OvfbaKy8n/CUiSsa/YkQFTmaxl+ExmWtcJuKVWe/POuuslEm1Z8yYgdraWqxfvx7bb7+92kcCoA8//DCtSZnlOFInVV1djZ133llNACvZJRn6u/HGG1UA98UXX6hgbdasWcw8EVFeYxBFVOAuuugiFbTMnDkTXq8Xn3/+OSZPnqwCq6OOOiqx3ffff68CnIEDB6pM1MSJE3HXXXfhD3/4Q1qPt2zZMnz55Zcq+KqpqcGQIUNUECXHlmE/CdSGDh2KV199FdOmTeuEZ0xE1DVYgEBUwCS7JMHKFVdcoQIoscsuu+Dwww9XNUjJJKCSAErIUNvee++N7777Lu3HlOPY2SsZ2pPC82OPPTZRNyW32223nQraiIjyGYMoogK2dOlSdSuZqGQSxEjGKFlVVVXKfWmTIEN66aqsrGxxnNbWZXJsIqJcwiCKqIBJqwMhRd3J5L79PSIiygyDKKICtu2226os1DPPPJNYJ72dpBfTlClTuvXciIjyHQvLiQrc7bffjuOPPx6NjY3Ycccd8Y9//EMNr0nBORERZY6ZKKICIxmm3XbbLXFfpmSR/kyapqmWBRJQzZ07V/VusslUKxMmTEg5jlydt88++7T7WAsWLFAF6g0NDW0e54ADDlDtDpIdcsghquA8edoXOY60YSAiyheawb9aRJSBa6+9Fj/88IP6+s4778yqa/m9996rGnWK6dOnY/z48fyZEFHOYxBFRERElAEO5xERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBEREREhff8fAE5bZP8+EzAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df = parcels.read_particlefile(\"output-quickstart.parquet\")\n", + "waterlevel = ds.isel(time=2).watlev.plot(cmap=\"magma\")\n", + "scatter = plt.scatter(df['x'], df['y'], c=df['t'], s=10)\n", + "plt.scatter(df['x'][:npart], df['y'][:npart], facecolors=\"none\", edgecolors='r', s=10) # starting positions" + ] + }, { "cell_type": "markdown", - "id": "6f087323", + "id": "d91ddeec", "metadata": {}, "source": [] } ], "metadata": { + "kernelspec": { + "display_name": "Parcels:default (3.14.6)", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.6" } }, "nbformat": 4, diff --git a/src/parcels/convert.py b/src/parcels/convert.py index c8cea9ce3..ea2d403de 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -567,6 +567,130 @@ def copernicusmarine_to_sgrid( return ds +def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: + """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files. + """ + import scipy.io as sio + import re + import parcels._sgrid as sgrid + import pandas as pd + ## First load the coordinates and data files (SWASH output - matlab binary format) + coord = sio.loadmat(coord_file) + x = coord["Xp"][0, :] + y = coord["Yp"][:, 0] + bot = coord["Botlev"] + + mat = sio.loadmat(data_file) + keys = [k for k in mat.keys() if not k.startswith("__")] + + time_keys = sorted(set( + (int(m.group(1)), int(m.group(2))) + for k in keys + for m in [re.search(r"_(\d{6})_(\d{3})$", k)] if m + )) + times = [t[0] + t[1] / 1000 for t in time_keys] + + n_layers = max( + int(re.search(r"Vksi_k(\d+)_", k).group(1)) + for k in keys if re.search(r"Vksi_k(\d+)_", k) + ) + n_w_layers = n_layers + + nx, ny, nt = len(x), len(y), len(times) + + watlev = np.full((nt, ny, nx), np.nan, dtype=np.float32) + vksi = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) + veta = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) + w = np.full((nt, n_w_layers, ny, nx), np.nan, dtype=np.float32) + + for ti, (ts_int, ts_dec) in enumerate(time_keys): + ts_str = f"{ts_int:06d}_{ts_dec:03d}" + for k in keys: + if re.match(rf"Watlev_{ts_str}$", k): + watlev[ti, :, :] = mat[k] + m = re.match(rf"Vksi_k(\d+)_{ts_str}$", k) + if m: + vksi[ti, int(m.group(1)) - 1, :, :] = mat[k] + m = re.match(rf"Veta_k(\d+)_{ts_str}$", k) + if m: + veta[ti, int(m.group(1)) - 1, :, :] = mat[k] + m = re.match(rf"w(\d+)_{ts_str}$", k) + if m and int(m.group(1)) < n_w_layers: + w[ti, int(m.group(1)), :, :] = mat[k] + + t0 = pd.Timestamp("2026-06-01 00:00:00") + time_dt = np.array([t0 + pd.to_timedelta(t, unit="s") for t in times], dtype="datetime64[ns]") + + ds = xr.Dataset( + { + "watlev": (["time", "y", "x"], watlev), + "U": (["time", "depth", "y", "x"], vksi), + "V": (["time", "depth", "y", "x"], veta), + "W": (["time", "depth_f", "y", "x"], w), + "botlev": (["y", "x"], bot), + }, + coords={ + "time": time_dt, + "depth": np.arange(1, n_layers + 1), + "depth_f": np.arange(0, n_w_layers), + "y": y, + "x": x, + }, + ) + ds.attrs.update(source="SWASH version 11.01ABC", project="progWave", run="A14", Conventions="CF-1.8") + + n_layers = ds.sizes["depth"] + n_layers_f = ds.sizes["depth_f"] + if n_layers_f != n_layers: + raise ValueError( + f"Expected depth_f to match depth in length (got {n_layers_f} vs {n_layers})" + ) + + depth_centers = np.array( + [total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32 + ) + depth_interfaces = np.array( + [total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32 + ) + + # Rename x/y -> lon/lat: Parcels' from_sgrid_conventions expects these names + # literally, even on a flat/Cartesian mesh (units stay in meters). + ds = ds.rename({"x": "lon", "y": "lat"}) + + ds = ds.assign_coords({ + "depth": ("depth", depth_centers), + "depth_f": ("depth_f", depth_interfaces), + }) + + ds["time"].attrs.update(axis="T") + ds["lon"].attrs.update(axis="X", units="m") + ds["lat"].attrs.update(axis="Y", units="m") + ds["depth"].attrs.update(axis="Z", units="m", positive="down") + ds["depth_f"].attrs.update(axis="Z", units="m", positive="down") + + if "grid" in ds.cf.cf_roles: + raise ValueError("Dataset already has a 'grid' variable (cf_role grid_topology).") + + ds["grid"] = xr.DataArray( + 0, + attrs=sgrid.SGrid2DMetadata( + cf_role="grid_topology", + topology_dimension=2, + node_dimensions=("lon", "lat"), + node_coordinates=("lon", "lat"), + face_dimensions=( + sgrid.FaceNodePadding("lon", "lon", sgrid.Padding.NONE), + sgrid.FaceNodePadding("lat", "lat", sgrid.Padding.NONE), + ), + vertical_dimensions=( + sgrid.FaceNodePadding("depth", "depth_f", sgrid.Padding.LOW), + ), + ).to_attrs(), + ) + + return ds + + # Known vertical dimension mappings by model _FESOM2_VERTICAL_DIMS = {"interface": "nz", "center": "nz1"} _ICON_VERTICAL_DIMS = {"interface": "depth_2", "center": "depth"} diff --git a/src/parcels/tutorial.py b/src/parcels/tutorial.py index 49a4a8b6b..99d4f3df6 100644 --- a/src/parcels/tutorial.py +++ b/src/parcels/tutorial.py @@ -17,7 +17,7 @@ def list_datasets() -> list[str]: return _list_remote_datasets(purpose="tutorial") -def open_dataset(name: str): +def open_dataset(name: str, download_only=False): """Download and open a tutorial dataset as an :class:`xarray.Dataset`. Use :func:`list_datasets` to see the available dataset names. @@ -33,4 +33,4 @@ def open_dataset(name: str): xarray.Dataset The requested dataset. """ - return _open_remote_dataset(name, purpose="tutorial") + return _open_remote_dataset(name, purpose="tutorial", download_only=download_only) From 4e1a79cdbd6a408ce20dbf280fb8aeb740dc3694 Mon Sep 17 00:00:00 2001 From: ChitYanToe Date: Wed, 22 Jul 2026 17:42:28 +0200 Subject: [PATCH 03/15] Fixing SWASH tutorial 1. Fixed the SWASH data files download in remote.py 2. Changed the source path of the SWASH files in the tutorial --- docs/user_guide/examples/tutorial_swash.ipynb | 47 +++++++++---------- src/parcels/_datasets/remote.py | 29 ++++++++++-- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index 9a19f33d9..bfb81a423 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -58,12 +58,12 @@ "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ - "
\n", - "
\n", + "
\n", + "
\n", "
\n", "" - ] - }, - "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 version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\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, Bokeh, 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 const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\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.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/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 } 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 = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\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, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\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": "f0dc6f77-9e34-4bee-aeaa-a69cb9197888" - } - }, - "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 version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\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, Bokeh, 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 const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\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.9.3/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 } 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 = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\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, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\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/2v/5l455kn13xq41s2x_zl1chlr0000gn/T/ipykernel_87891/208841806.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" - ] - } - ], + "outputs": [], "source": [ + "import glob\n", + "\n", "import matplotlib.pyplot as plt\n", "import matplotlib.tri as mtri\n", "import numpy as np\n", @@ -194,35 +24,24 @@ "import xarray as xr\n", "\n", "import parcels\n", - "import parcels.tutorial\n", - "import glob\n" + "import parcels.tutorial" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "1a044f75", + "execution_count": null, + "id": "2", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['/Users/YanTo001/Library/Caches/parcels/data-matlab/F1ALL.mat', '/Users/YanTo001/Library/Caches/parcels/data-matlab/F1GRD.mat']\n" - ] - } - ], + "outputs": [], "source": [ - "grid_file = parcels.tutorial.open_dataset(\n", - " \"SWASH_data/data\", download_only=True\n", - ")\n", - "print(grid_file)\n" + "grid_file = parcels.tutorial.open_dataset(\"SWASH_data/data\", download_only=True)\n", + "print(grid_file)" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "81c491ae", + "execution_count": null, + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -231,36 +50,31 @@ "# ds = parcels.convert.swash_to_sgrid(grid_file, result_file, total_depth=8.0)\n", "ds = parcels.convert.swash_to_sgrid(grid_file[1], grid_file[0], total_depth=8.0)\n", "\n", - "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n" + "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "7fd5d3d1", + "execution_count": null, + "id": "4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in output-quickstart.parquet\n", - "Integration time: 2026-06-01T00:00:15 100%|██████████| [00:00<00:00, 909.13it/s]\n" - ] - } - ], + "outputs": [], "source": [ "npart = 150 # number of particles to be released\n", "# release particles in a line along a meridian\n", - "lat = np.linspace(1, 16, npart) ## y\n", - "lon = np.repeat(15, npart) ## x\n", + "lat = np.linspace(1, 16, npart) ## y\n", + "lon = np.repeat(15, npart) ## x\n", "time = np.repeat(ds.time.values[0], npart)\n", - "layerI = 0 # at which water depth, the particles are released\n", + "layerI = 0 # at which water depth, the particles are released\n", "z = np.repeat(ds.depth.values[layerI], npart)\n", - "pset = parcels.ParticleSet(fieldset=fieldset, pclass=parcels.Particle, t=time, x=lon, y=lat, z=z)\n", + "pset = parcels.ParticleSet(\n", + " fieldset=fieldset, pclass=parcels.Particle, t=time, x=lon, y=lat, z=z\n", + ")\n", "kernels = [parcels.kernels.AdvectionRK2]\n", "\n", - "output_file = parcels.ParticleFile(\"output-quickstart.parquet\", outputdt=np.timedelta64(5, \"s\"), mode=\"w\") # \n", + "output_file = parcels.ParticleFile(\n", + " \"output-quickstart.parquet\", outputdt=np.timedelta64(5, \"s\"), mode=\"w\"\n", + ") #\n", "pset.execute(\n", " kernels,\n", " runtime=np.timedelta64(20, \"s\"),\n", @@ -271,41 +85,22 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "7933d20b", + "execution_count": null, + "id": "5", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAHGCAYAAABU9964AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAbv5JREFUeJzt3Qd8G/X5P/DPadqWR+wMErJDwsggJIGwEnYZYe89yujgB2UU/pCWUaBtCqWFsmdpyyi0lBn2SFghrAQII0ASsoczvWTNu//r+d6dLHnFks62JH/evA5Z57vTSXasR8/3ueerGYZhgIiIiIjS4kpvcyIiIiJiEEVERESUIWaiiIiIiDLAIIqIiIgoAwyiiIiIiDLAIIqIiIgoAwyiiIiIiDLAIIqIiIgoAwyiqFP9/e9/x2uvvcZXmYiICo7GjuWUrQcffBBDhw7FwQcf3OJ7w4YNw5QpU/DYY48V7Au9Zs0afPLJJ1i3bp16Hfbcc0+UlZW1uf3nn3+OTz/9FB6PB/vssw9GjBjhyHFjsRjee+89/PDDD+jfvz/233//drfP5ty64/G+//57vPnmm4hEIrj44ovhdrsT3/vggw/Ua7U1Pp8PF154YUaPn862ThzDicdr7zXrjMcj6mmYiaKsXXvttXjiiSda/d65556LQw89tCBf5U2bNuGoo47CkCFDcPvtt6s38SuuuALbbbcdHn/88Rbbx+NxnHnmmSqofPfdd/HSSy9h9OjRuPHGG7M6rvjwww+x4447qjfKzz77DE8++SQmTZqEWbNmdei5dPTcuuPxnn76aUyYMAFHHnkk/vKXv+Cyyy5DNBpN2Wbz5s1YunRpYvniiy/Udo8++mjK+mXLlqX9+Om+Ntk+XyceryOvmZOPR9RjSSaKKBvbbLONcfbZZ/e4F/Hbb781hg4danzwwQeJdfF43DjnnHMMl8tlzJ8/P2X73//+94bb7TY++uijxLrHH39c5q40nn322YyP+9133xmBQMCYPn26oet6Yv369euNzz77rEPPpaPn1h2P9+qrrxrz5s1TX59++ulqm8bGxnaP/8MPP6jtfv3rX2f9+Ols68TzdeLx0nnNnHg8op6KQRRlLBaLGbfddptRVlZm7LbbbuprWZ588snENg8//LD6g57snnvuMd566y31tQQK999/v/Huu++mbDNnzhzjvvvuM15++eWUN+pksl62e+CBB4wHH3zQ+PLLL7v0p7lhwwZj9erVLdZLICFvQNdff33Ka9WvXz/jsMMOa7H9iBEjjKlTp2Z0XHHUUUcZo0aNUoFWJtI5t+54vGROBFHpPH665yqBiPwbWL58ebc9XjqvWTY/CyIyDE93Z8Iof0kQLkMkMhxQX1+vvm5OhgRkmOCQQw5JrJs+fTpOOeUUNQQo9T7l5eW45JJL1NDfnXfeiZ/+9KfYsGEDKisr8cwzz6jhQLlNtnz5cpxwwglYsmQJDjroILVOhiyOPfZYVcwudR1tefHFF7F48eK062ea6927d6vra2tr1a2u64l133zzDaqrq9Vr0dzUqVPVMF04HIbf70/ruHV1dWr45dJLL8X69evx1ltvoaamBqNGjVI1Su3VwGRybl39eJ0hncdP91xff/11Nbw9duxYDB48uFser7NeCyJqiUEUZUwCFanZkXqYyZMnq6876oUXXsAdd9yhAiHxr3/9C2effbb6gy1B06mnnqrWS6B1+umn45133sG+++6r1knQdsQRR6ht5U2gX79+av1XX32FXXfdFTvttJMK1Nry8MMP4/nnn9/qOQYCgXaDqLZIDYo4/PDDE+t+/PFHdTto0KAW28ubnxRpS2AowUg6x/3666/V6zFv3jyMHDkSBxxwgCru/u1vf4uBAweqgFGK+9uTzrl19eN1hnQeP91z3X333dUHAqln667H66zXgohaYhBF3WKbbbZJBFDipJNOUkHURx99hIceeiixXrY544wzVMGyHUS98sorWLBggQre7ABKyKdxOc4DDzzQbhAlxbZbe6MXmXz6/tvf/oaZM2firLPOUm9wtoaGBnVbXFzcYh97nb1NOsfdsmWLupXX59lnn8Uxxxyj7kuGbpdddsFpp52GOXPmJF637777LrFv3759VYCazrl19eN1hnQeP91z/clPfqKW7ny8dHT3z4Io3zGIom4xZsyYlPtFRUWoqKhosV6G1KqqqrBq1arEOrkaTEhW5K677lLDirIIGWKSYcX2hiDOO++8TnhGwFNPPYXLL79cDY3ce++9rb4hyXk1Z69r7Y1sa8eV103I1VR2QCPk8nQJPmV7CXDk/j//+U91LNv48eNVUJPOuXX143WGdB7fiXPt6sdLR3f/LIjyHYMo6halpaWtDg+2tT758mz767Vr1ybqhGw77LCD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YCaKiIgoX7CwPKcwiCIiIsoXDKJySrcO54XDYTz22GOYMmUK+vTpgw8++KDFNsOGDVPfa75cddVVbR73iiuuaLH91KlTkTPD2QU0HC0N+7qqaZ9UAeg98DUmImoRRGW7UP5noq699lqsWrUKF198MU455RREo2Y37mSfffaZmtzR9uGHH+Koo47CgQce2OZx6+vrsddee+Hvf/97Yp3Hw6QbEREROadbI4ubb75ZtclfuXJlm9v07t075f5zzz2nslMHHXTQVmeolgwUERFRwTAcyCTJMSj/h/PSnWdIMkz/+c9/cN5558Hlav/UZ82ahSFDhmDcuHG48MILsX79+izPloiIqHtphu7IQj2wxcFTTz2FxsZG/PSnP213uwEDBuDWW2/F7Nmzcc8992D+/PnYe++9EQwG263Pqq2tTVmoYxOJZjsRKhERUT7Kq0Khhx9+GNOmTcPAgQPb3e76669PfD1ixAg8//zzGDx4MJ588kmce+65re4zY8YM3HDDDY6fMxERkWN4dV5OyZtM1LfffquKyi+44IK09+3Xr58a2lu4cGGb20yfPh01NTWJZcWKFVmeMRERkcPsy4+zXahnZaIeeughlYGSTFS6ZBhv9erV6Nu3b5vb+P1+tXQ2TvtCRERUGPIiEyWtDx599FE1FOd2u1t8/7LLLkv0gZLaJslWLVmyRN2XgvJzzjlHtTg49dRTu/zciYiIHMM+UTmlW4MoqVGSNgQ777yzun/00Uer+7fcckvKdi+88AI2btzYZj1TXV0dNm/erL6WbJI07zziiCMQCARUOwQZnnvvvfcwaNCgLnhWREREnYRBVE7p1uG8Y489ttV+TyUlJSn3DzvsMJVRqqqqavU4t99+O2KxWOL+2WefrZZQKISioqJOOHMiIiLq6bo1iOpoHZIEVc0Dq2SlpaWtri/kACrNFluOpyCzfXyiQsfWH9QpZAaPbPs8Jc0CQj2ksJyIiKjHY4uDnMIgioiIKF840aKALQ561tV5RERERLmGmag8le2Qtu7Q4+uG88VRGjheT0TUKg7n5RQGUURERPlCisolkMr2GOQIDucRERERZYCZqC5m1/O5te5pMWDkULTND0NEROnRdF0t2ch2f2rCIIqIiCiv+kRlWTfKPlGO4XAeERERUQaYiSIiIsoXvDovpzCIylNtZmO3klvUMkxBtjWCzqkt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2pSFiIja7ljePBNl9HXhvcO2w1e79ld9okrqoogEemDaowuC82yXniwajapyIolVJMGybt06XHDBBYXX4sAu+orFYi1eALfb7dg+YsaMGbjhhhscOGsiosIXrvM33amzqmzW65j6ymK1SjJRDWVeeBt69hu20wwHaprMCrae54MPPsBjjz2G//73v2hoaMCRRx6phvUOO+wweL3ergui5DJBqTt6//33sXnz5hbfnz17NpwwaNAgdSttCpLJ/YEDBzq2j5g+fTouv/zyxH3JRA0ePDir8yciKlTekmjTnXKrJqqfC+9NHo6qdUGM/WQNSusiiDITRTlAWh7ts88+6qo8ufjs+OOPR3l5edbHzSiI+tnPfqbaERxzzDEYM2YMOktZWRkmTpyI1157DSeffHIioyRX811yySWJ7TZu3KiG47bddtsO79Na5b4sRES0dZEtSTVRtWZuxKjWsc/MpkxUfbkPPl6d5ygWlmfG5/NhxYoVKk5wUkZB1PPPP6/aB2y//fZZPXh9fT02bNiQyBrJrTTd7NWrl1qEtCeQ/k6TJk3Cnnvuib/85S9qWO6Xv/xl4jhXXXUV5s6di6+++qrD+xARUea8gaSSiTIrEyU1UXsNR8WaRoz7eDVKayKIlLDZsZOcqGnqiTVRLpcrEUAFg0F1AdqOO+6Y/XEz2amkpEQVamfr1VdfVc01pbhL+kVdffXV6v4//vGPxDZHH320GrN86qmnVK8nyTi9++67KY8vXycP1XVkHyIiylx4Sxs1Uc8vxs4fr4auMlF++II5MTEGEWpqatQIVWlpKXbZZZfEKyKjanYSpksyUdJPQYqw//rXv7ZbrL01ki3qSBdxGbuUpS1/+tOf0t6HiIgy50nORCXVRL2/53CUrw5inNRE1YYRYZ8oR0lReLaF4T21sPyKK65Qddwffvgh9t1338T6s846CzfeeCP+85//dE0QdfHFF2PnnXdWjaokg9S8uaYM9RERUeGK1LReEzXl+aSaqDI/fLw6z1GcgDhzzz33nOpQ3rwF0h577IGf//znGR0zoyDqpz/9KSorK3HiiScmapeIiKjn8FVEWmai+rrw/t4jULE6iLFSE1UfZsdyygnSUFOyUH379lWTETfvONB8XacGUdLaQOahGzFiREYPSkRE+S1a722ZiVqvY8pzi1IzUfU9c+ios7CwPPPCcpne5ZVXXlF9oZLJfL+777571wVRMhOyE/0ViIgoPxVVhprulGtWTZQb7+41HJV2JkpqotgnylGsicrcH//4R3Uh27nnnot4PK7qqV9++WU1/ct7772X0TEzumxChvF++9vfIhJJSucSEVGPEQ0mZ6KsUuXqOPZ5bpFqb6Cuzqvww89MVKfURGW79ETTpk3DSy+9hEWLFqlhvTvuuAMVFRVqdG3y5Mldl4mSOWcWLlyoJhqWrt7NC8szvVSQiIjyg7+q9UzU7D1GoGptPcZ9vAYBXp1HOWbq1KlqcUpGQVSmVexERFQY4iF3i0yUUR3Hfi/80DR3XrlkotgnqlCG86T4unnSxIl9MjlursgoiLr00kudPxMiIsob/l6hVvpEuTF78nD0XteQ6BMVZZ+ovG9xsHjxYpU8kabVMn2K1BXJUFhJSUlW+2Ry3HRIo+0ddtihQ9vKtG/fffdd5wVR9913H37xi184vi0REeWfeNjdSp+oOPabmXx1no9X5+U5qX2Wq9nGjRuHNWvWqKnaDj/8cFx44YUps4uku08mx02Xx+PBNddc0+FtM3qMjm4o8851NDBKZ1siIso/3vJoK32i3Hhnt2HoU92AMZ+uRWmtzJ3XQ6uYC6TFwcyZM1Uh9uzZs1WTSll+97vf4ZxzzsGf//xnVaCdyT6ZHDddMqPK+eefr76Wq/HuvPPOVkfS7O9lIq3Qa8qUKRk9CBERFRY97EKijMXKRGF9HPu+nNSxvNwHPzuWO0pC0mzD0nT2lylSRo0alZi8V+y///4q8Pjkk0/UFW+Z7JPJcbMRjUbV/LytBVHyvenTp2dUqtThIOr666/v8EEPOuigtE+EiIjyh6c4BmnyrAIpOxPVx40Pdh2KquoGjJ63DqV1EfaJymG1tbUt6oJkSbZu3boWWSH7/rp161o9bkf2yeS4nUVqs5pPBeN4ECVpNiIiIhELeZoyUXVWJmpDHFNeXdKUiSr1ce48h8mVddkO59lX50mLoubJktbe62XKlJT9rSlStHauqOvIPpkcN11SezV69Gh1bPl65MiRKd+XzJfUZJ133nkZHT+zSioiIurR3D69KRNV2pSJ+nDCEFRuCGKn+etQ2sBMlNMk7NAdOIZYsWJFyuwjzbNQYsCAAZgzZ07Kuurq6sTsJa3pyD6ZHDfTuqgrrrgCsVgMl112mfo6mdfrxfDhw9VQYiYYRBERUdoi9UlvH/VNmai93vixqU9UwAdvY372/+kJJIDa2hRuUgt9yy23YPny5RgyZIha99Zbb6mr2SYndfkOhUKqTYHMUdeRfTp6XCeCKLnQTbJeAwcOxLHHHgsnsQsaERFllIlKCJgdy43eLnx0wFAsHN8Pbt1ASUOUV+c5zDA0R5aOstsQXHDBBViyZAk+/vhj1TZA7ldVValtpD1BcXExnnjiiQ7v05FtnCTBndMBVMaZqFtvvbVFSqwj3yMiosIQrU0a+mmwqmw26tj97WUqoJJMVLDUCw8zUY5ycjivIyQzJJP0XnLJJdh1111VsCRNMWfMmJHYRmqYZChQsj4d3acj23SGBQsW4IUXXlBDmTLElzysd++993ZNEHXllVe2GSi19z0iIioMmldvKiwPmF8YlS7M23kgyjeFMGrBepWJigayfcun7u5YPmjQIPzvf/9r8/u9e/dWw3np7NPRbZz03//+F2effbaaO+/NN9/EEUccgXnz5mHlypU47rjjur8mKpvLBImIKH9EavxNheVB66qqzTomvbMikYlqDHjhCbFqhHLDddddh8cee0z1oOrVqxeef/55dcWeNOQsKyvr/CAq+dLA5pcJStHW6tWrcfrpp2d0IkRElD/cvnhTJqqkKRP11ej+KNkcxnbfbESRZKKKmYkqlAmI81k8Hle1V0ceeaQafpQGm7JOiuFvuukmVeh+9913d24QZQ/TybQurV0mOGzYsIwvEyQiovwRrfO1moka98FqlYnSXUCoxAtvuOe9YRfacF4hiEajKniSWEXIqJmMnm2//fYqmGpsbMzouGkFUfZ8eH369MEJJ5yQ0QMSEVH+c/lbyURVaFi40zYo2hzB8O82oSgomage+I5NOU8yUlIfdfLJJ+M///mPqpPKREaD1QygiIh6NjsTpdiZqBoDO81di2HfbYKuAaFiL9whZqI6Yzgv26Wn8fl8ePbZZxP3//rXv6oWC//4xz9Us81MrszLqrD8m2++wXPPPacaZSVfJigeeuihTA9LRER5wBuQ4ZFmmahyDT/s0A/+zVEMXbQJ/lAMsSJmopzE4bzMyFDevvvum7hfUVGBBx54ANnKKIiSHguSApMTeu2113D00UerywSl74LTMy8TEVHuiYVcTTVRjVYmqtbA9p+sM2uiNCDs98AT6XlZD8o94XBYXZG32267Yb/99lPL3nvvjZKSkq4PomSSwocffhinnXaaiu4kIyVFWxdeeGGLCQWJiKjw+MqSMlHFZmWIUaZh0ci+KK6JYNCSLfBHYoj5mYlyEjNRmSkqKsKsWbMwe/Zstdx+++0qbrGDKrko7qCDDuqamqiFCxeq7JPddVSq2qXi/fe//73KUhERUWGLNXqaaqIarQ/PdQZGza/GwCVbEpkod6Q7z7LwsCYqc5J5+u1vf4s33ngDW7ZsUUGV1EPdfPPNqvFml2WipDNpIBBIzMT8ww8/YOedd87qMkEiIsof3rJIUybKb31RqmHx8N4oro1i22U18IdjiJtXlBPlhO+//z6RkZJbGeaTefwyyUI50rFcLhM877zzVJPNZ555JuPLBImIKH/Ew+6mmqiwlZKqN7Ddgg0qCyVLxOeGO8qaKCfJa55tn6dEBrEHiUQiqpdlMBhUcYoM4Um/ywkTJqjJiTOV0Z4vvvhi4us//elPavJAuUxw22235ZV5REQ9gDcQa8pEFVlfBDT8uFNvrB9YBs0AfJE4dE8PfMfuggmIs116Gk3T1AiajKTV1tairq4O9fX1qi4qGxllopLHDmW+mUz7KxARUX6KJWeiQlag1GBg2LcbYWhQi2SiXMxEOcowNLVke4yexuv1qtIjmZ7OHs6TOfNk8uE999wTBx54oKqXShdnhiQiorR5kjuW+6wvijWsHFWJjf1LVSbKG2UminKLjJhJ+dFtt92GO++8UzUPf/fdd9X8eZ2aiZKxxI5aunRpRidDRFQoXBJFFLBYMCkTFbGea6OBgYs2w5CVGhDzuOCK9bysR2dyYjiuJw7nGYahrsqzC8o//fRT+P1+lYW64YYbcMABB3RuEHXNNddk9ABERFR4XB6jZSaqSMPaAeXwN0RRVR2EJ26wJsph7BOVGbkK77jjjlNBk5Qk3XrrrZg8eXJiQuJMdTiIkrFDIiIiEQt6Wmaiwga2WV6rOparTJTbBXecmSjKjWabmzdvzjpoao41UURElLaUSWztTJQP2LRtKep6FckG8MR16G6+uE4yHFp6Iq/DAZQjfaKIiKjniTUkvX1ErbflCFC5tiHxLh2T/jvx7jm/wh7Oyy67l22fKWrCTBQREWUg6Y3cY33tBer6FKGhzGeulrlUmYmiAsZMFBERpS0WTnr7iNupJyCwKWQ2iVKZKGldzhfXSU4MxzER1YOCqPHjx6OhoaHFeunzIJcltua6667DE088kbJu++23x8svv9xp50lE1JOkDCi5rXtuIFThgxbS4Y3E4TYMGG6+ZTuJV+fllpwPop599lnokhK2zJs3DyeffDJ22223Nveprq7GqFGjVCMtm/SDICIiZ8RD7pZFNnHAXx9N1EHp0KDJJHpEBSrng6gRI0ak3P/LX/6CgQMHqlmX2yNz5IwcObKTz46IqGfylkea+kTZty4gWuSBFjHgC8fhkkxUgTcd7Wpstplb8qqwvLGxEf/+979x7rnnwu1uv1rx/fffx84776xma/7Nb36jJhskIiJnaDBUn6gUBuCO6HDHzdEDXdOg9cB52jqTvOZOLNRDMlHJnn76aRUMnXfeee1uV1VVhauuugr77rsvVq1apbqtz5w5U7V59/nMq0Za62Yqi01meSYi6uppYrK9fL2rFPdPqlVNelPW3TKEZ36dH88k//pzyTBptsegHhhEPfzww/jJT36CoUOHtrudTCRoZ6omTpyICRMmYPjw4XjyySdx1llntbrPjBkz2ixUJyKiVJqnlcvuNPMNWrOH8NSlZEx7UOHKm+G8RYsWqZmWL7jggq1u23yob9CgQWoC5a+//rrNfaZPn46amprEsmLFCkfOm4ioEJUNaSVbrwOuuA6X3fJA5iHOk8xavuBwXm7Jm0zU3//+d/Tt2xdHHXVU2vvKMN3atWvRq1evNreRq/d4BR8RUcd4SqKtFpZHfG54reZQmmGAdeXOYmF5bsmLTFQ8Hsc///lPnHPOOa3OfSOF49OmTVNfRyIRXHnlldi4caO6HwwGceGFF6pjSGsEIiLKjubSVRCVGKmzb3XAH4rBEzV7HOiupvoookKUF5koaZK5evVqnH/++W32hVq+fLn6WoIsGb6TK/Okv5TM2iwNO996660W7RKIiCh93rIIXMlVE1YmynBrCJb6VHsD1eKAk7Q5js02c0teBFF77703lixZoorD2yoKD4VC6mtN03DJJZeoZd26daioqEBRUVEXnzERUeHylEZUFioxnGdfjRczUFYTTjTflEyUK8aaKCdx2pfckhdBlLQskKUtUivVmm222aYTz4qIqG12qwK7dYFTnD5eJkoH1TUFUEnTvsS9LmypKlZDet7NITMTxRiKClheBFFERJRLmgVyMfO+O6qjckNQfVvW6G4XM1EO43Be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LlaPLedQXl6OvfbaKy8n/CUiSsa/YkQFTmaxl+ExmWtcJuKVWe/POuuslEm1Z8yYgdraWqxfvx7bb7+92kcCoA8//DCtSZnlOFInVV1djZ133llNACvZJRn6u/HGG1UA98UXX6hgbdasWcw8EVFeYxBFVOAuuugiFbTMnDkTXq8Xn3/+OSZPnqwCq6OOOiqx3ffff68CnIEDB6pM1MSJE3HXXXfhD3/4Q1qPt2zZMnz55Zcq+KqpqcGQIUNUECXHlmE/CdSGDh2KV199FdOmTeuEZ0xE1DVYgEBUwCS7JMHKFVdcoQIoscsuu+Dwww9XNUjJJKCSAErIUNvee++N7777Lu3HlOPY2SsZ2pPC82OPPTZRNyW32223nQraiIjyGYMoogK2dOlSdSuZqGQSxEjGKFlVVVXKfWmTIEN66aqsrGxxnNbWZXJsIqJcwiCKqIBJqwMhRd3J5L79PSIiygyDKKICtu2226os1DPPPJNYJ72dpBfTlClTuvXciIjyHQvLiQrc7bffjuOPPx6NjY3Ycccd8Y9//EMNr0nBORERZY6ZKKICIxmm3XbbLXFfpmSR/kyapqmWBRJQzZ07V/VusslUKxMmTEg5jlydt88++7T7WAsWLFAF6g0NDW0e54ADDlDtDpIdcsghquA8edoXOY60YSAiyheawb9aRJSBa6+9Fj/88IP6+s4778yqa/m9996rGnWK6dOnY/z48fyZEFHOYxBFRERElAEO5xERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBERERFlgEEUERERUQYYRBEREREhff8fAE5bZP8+EzAAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "df = parcels.read_particlefile(\"output-quickstart.parquet\")\n", "waterlevel = ds.isel(time=2).watlev.plot(cmap=\"magma\")\n", - "scatter = plt.scatter(df['x'], df['y'], c=df['t'], s=10)\n", - "plt.scatter(df['x'][:npart], df['y'][:npart], facecolors=\"none\", edgecolors='r', s=10) # starting positions" + "scatter = plt.scatter(df[\"x\"], df[\"y\"], c=df[\"t\"], s=10)\n", + "plt.scatter(\n", + " df[\"x\"][:npart], df[\"y\"][:npart], facecolors=\"none\", edgecolors=\"r\", s=10\n", + ") # starting positions" ] }, { "cell_type": "markdown", - "id": "d91ddeec", + "id": "6", "metadata": {}, "source": [] } diff --git a/src/parcels/_datasets/remote.py b/src/parcels/_datasets/remote.py index 6ac760b3c..9a8b461fd 100644 --- a/src/parcels/_datasets/remote.py +++ b/src/parcels/_datasets/remote.py @@ -152,13 +152,14 @@ def __init__(self, pup: pooch.Pooch, path_relative_to_pup: str, pre_decode_cf_ca def open_dataset(self, download_only=False) -> xr.Dataset: self.download_relevant_files() - if download_only: ## Chit Yan Toe + if download_only: ## Chit Yan Toe import glob + matches = sorted(glob.glob(f"{self.pup.path}/{self.path_relative_to_root}")) return matches if len(matches) != 1 else matches[0] # if download_only: # return f"{self.pup.path}/{self.path_relative_to_root}" - + with xr.set_options(use_new_combine_kwarg_defaults=True): ds = xr.open_mfdataset( f"{self.pup.path}/{self.path_relative_to_root}", @@ -181,7 +182,7 @@ def open_dataset(self, download_only=False) -> xr.Dataset: # self.pup.fetch(file) # return - def download_relevant_files(self) -> None: # Chit Yan Toe + def download_relevant_files(self) -> None: # Chit Yan Toe for file in self.pup.registry: if fnmatch.fnmatch(file, self.path_relative_to_root): self.pup.fetch(file) diff --git a/src/parcels/convert.py b/src/parcels/convert.py index ea2d403de..3703ef0b1 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -568,12 +568,14 @@ def copernicusmarine_to_sgrid( def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: - """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files. - """ - import scipy.io as sio + """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files.""" import re - import parcels._sgrid as sgrid + import pandas as pd + import scipy.io as sio + + import parcels._sgrid as sgrid + ## First load the coordinates and data files (SWASH output - matlab binary format) coord = sio.loadmat(coord_file) x = coord["Xp"][0, :] @@ -583,17 +585,12 @@ def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: mat = sio.loadmat(data_file) keys = [k for k in mat.keys() if not k.startswith("__")] - time_keys = sorted(set( - (int(m.group(1)), int(m.group(2))) - for k in keys - for m in [re.search(r"_(\d{6})_(\d{3})$", k)] if m - )) + time_keys = sorted( + set((int(m.group(1)), int(m.group(2))) for k in keys for m in [re.search(r"_(\d{6})_(\d{3})$", k)] if m) + ) times = [t[0] + t[1] / 1000 for t in time_keys] - n_layers = max( - int(re.search(r"Vksi_k(\d+)_", k).group(1)) - for k in keys if re.search(r"Vksi_k(\d+)_", k) - ) + n_layers = max(int(re.search(r"Vksi_k(\d+)_", k).group(1)) for k in keys if re.search(r"Vksi_k(\d+)_", k)) n_w_layers = n_layers nx, ny, nt = len(x), len(y), len(times) @@ -642,25 +639,21 @@ def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: n_layers = ds.sizes["depth"] n_layers_f = ds.sizes["depth_f"] if n_layers_f != n_layers: - raise ValueError( - f"Expected depth_f to match depth in length (got {n_layers_f} vs {n_layers})" - ) + raise ValueError(f"Expected depth_f to match depth in length (got {n_layers_f} vs {n_layers})") - depth_centers = np.array( - [total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32 - ) - depth_interfaces = np.array( - [total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32 - ) + depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) + depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) # Rename x/y -> lon/lat: Parcels' from_sgrid_conventions expects these names # literally, even on a flat/Cartesian mesh (units stay in meters). ds = ds.rename({"x": "lon", "y": "lat"}) - ds = ds.assign_coords({ - "depth": ("depth", depth_centers), - "depth_f": ("depth_f", depth_interfaces), - }) + ds = ds.assign_coords( + { + "depth": ("depth", depth_centers), + "depth_f": ("depth_f", depth_interfaces), + } + ) ds["time"].attrs.update(axis="T") ds["lon"].attrs.update(axis="X", units="m") @@ -682,12 +675,10 @@ def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: sgrid.FaceNodePadding("lon", "lon", sgrid.Padding.NONE), sgrid.FaceNodePadding("lat", "lat", sgrid.Padding.NONE), ), - vertical_dimensions=( - sgrid.FaceNodePadding("depth", "depth_f", sgrid.Padding.LOW), - ), + vertical_dimensions=(sgrid.FaceNodePadding("depth", "depth_f", sgrid.Padding.LOW),), ).to_attrs(), ) - + return ds From 32e3927396105c5531cfc81295692201bbd3b5e6 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 11:18:39 +0200 Subject: [PATCH 05/15] Cleaning up remote.py --- src/parcels/_datasets/remote.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/parcels/_datasets/remote.py b/src/parcels/_datasets/remote.py index 9a8b461fd..14b9a390b 100644 --- a/src/parcels/_datasets/remote.py +++ b/src/parcels/_datasets/remote.py @@ -1,6 +1,7 @@ import abc import enum import fnmatch +import glob import os from collections.abc import Callable from pathlib import Path @@ -152,13 +153,9 @@ def __init__(self, pup: pooch.Pooch, path_relative_to_pup: str, pre_decode_cf_ca def open_dataset(self, download_only=False) -> xr.Dataset: self.download_relevant_files() - if download_only: ## Chit Yan Toe - import glob - + if download_only: matches = sorted(glob.glob(f"{self.pup.path}/{self.path_relative_to_root}")) return matches if len(matches) != 1 else matches[0] - # if download_only: - # return f"{self.pup.path}/{self.path_relative_to_root}" with xr.set_options(use_new_combine_kwarg_defaults=True): ds = xr.open_mfdataset( @@ -176,13 +173,7 @@ def open_dataset(self, download_only=False) -> xr.Dataset: ds = xr.decode_cf(ds) return ds - # def download_relevant_files(self) -> None: - # for file in self.pup.registry: - # if self.v3_dataset_name in file: - # self.pup.fetch(file) - # return - - def download_relevant_files(self) -> None: # Chit Yan Toe + def download_relevant_files(self) -> None: for file in self.pup.registry: if fnmatch.fnmatch(file, self.path_relative_to_root): self.pup.fetch(file) From 3328238d56b2a1f9c2dbbcd4aa3e71582f3939ec Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 11:41:29 +0200 Subject: [PATCH 06/15] Simplifying swash tutorial code --- docs/user_guide/examples/tutorial_swash.ipynb | 61 ++++++++----------- docs/user_guide/index.md | 1 + 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index 73a2a6e0b..b7905ddb1 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial for using SWASH data in Parcels" + "# 🖥️ SWASH tutorial" ] }, { @@ -15,13 +15,8 @@ "metadata": {}, "outputs": [], "source": [ - "import glob\n", - "\n", "import matplotlib.pyplot as plt\n", - "import matplotlib.tri as mtri\n", "import numpy as np\n", - "import uxarray as ux\n", - "import xarray as xr\n", "\n", "import parcels\n", "import parcels.tutorial" @@ -34,8 +29,7 @@ "metadata": {}, "outputs": [], "source": [ - "grid_file = parcels.tutorial.open_dataset(\"SWASH_data/data\", download_only=True)\n", - "print(grid_file)" + "swash_files = parcels.tutorial.open_dataset(\"SWASH_data/data\", download_only=True)" ] }, { @@ -45,12 +39,11 @@ "metadata": {}, "outputs": [], "source": [ - "# grid_file = \"/Users/YanTo001/Documents/GitHub/parcels-data/data-matlab/F1GRD.mat\"#\"/Users/YanTo001/Library/Caches/parcels/data-matlab/F1GRD.mat\"\n", - "# result_file = \"/Users/YanTo001/Documents/GitHub/parcels-data/data-matlab/F1ALL.mat\"#\"/Users/YanTo001/Library/Caches/parcels/data-matlab/F1ALL.mat\"\n", - "# ds = parcels.convert.swash_to_sgrid(grid_file, result_file, total_depth=8.0)\n", - "ds = parcels.convert.swash_to_sgrid(grid_file[1], grid_file[0], total_depth=8.0)\n", - "\n", - "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)" + "ds = parcels.convert.swash_to_sgrid(\n", + " data_file=swash_files[0], coord_file=swash_files[1], total_depth=8.0\n", + ")\n", + "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", + "fieldset.describe()" ] }, { @@ -60,26 +53,23 @@ "metadata": {}, "outputs": [], "source": [ - "npart = 150 # number of particles to be released\n", - "# release particles in a line along a meridian\n", - "lat = np.linspace(1, 16, npart) ## y\n", - "lon = np.repeat(15, npart) ## x\n", - "time = np.repeat(ds.time.values[0], npart)\n", + "npart = 10 # number of particles to be released\n", + "y = np.linspace(1, 16, npart)\n", + "x = np.repeat(15, npart)\n", + "\n", "layerI = 0 # at which water depth, the particles are released\n", "z = np.repeat(ds.depth.values[layerI], npart)\n", - "pset = parcels.ParticleSet(\n", - " fieldset=fieldset, pclass=parcels.Particle, t=time, x=lon, y=lat, z=z\n", - ")\n", - "kernels = [parcels.kernels.AdvectionRK2]\n", + "pset = parcels.ParticleSet(fieldset, x=x, y=y, z=z)\n", "\n", "output_file = parcels.ParticleFile(\n", - " \"output-quickstart.parquet\", outputdt=np.timedelta64(5, \"s\"), mode=\"w\"\n", - ") #\n", + " \"output-swash.parquet\", outputdt=np.timedelta64(5, \"s\"), mode=\"w\"\n", + ")\n", "pset.execute(\n", - " kernels,\n", + " parcels.kernels.AdvectionRK2,\n", " runtime=np.timedelta64(20, \"s\"),\n", - " dt=np.timedelta64(5, \"s\"),\n", + " dt=np.timedelta64(1, \"s\"),\n", " output_file=output_file,\n", + " verbose_progress=False,\n", ")" ] }, @@ -90,12 +80,15 @@ "metadata": {}, "outputs": [], "source": [ - "df = parcels.read_particlefile(\"output-quickstart.parquet\")\n", - "waterlevel = ds.isel(time=2).watlev.plot(cmap=\"magma\")\n", - "scatter = plt.scatter(df[\"x\"], df[\"y\"], c=df[\"t\"], s=10)\n", - "plt.scatter(\n", - " df[\"x\"][:npart], df[\"y\"][:npart], facecolors=\"none\", edgecolors=\"r\", s=10\n", - ") # starting positions" + "df = parcels.read_particlefile(\"output-swash.parquet\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "waterlevel = ds.isel(time=2).watlev.plot(cmap=\"magma\", ax=ax)\n", + "for traj in df.partition_by(\"particle_id\"):\n", + " ax.plot(traj[\"x\"][0], traj[\"y\"][0], \"wo\", markersize=5)\n", + " ax.plot(traj[\"x\"], traj[\"y\"], color=\"k\")\n", + "ax.set_xlim([14, 16])\n", + "plt.show()" ] }, { @@ -107,7 +100,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Parcels:default (3.14.6)", + "display_name": "Parcels:docs (3.14.6)", "language": "python", "name": "python3" }, diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 06a578d79..703f72cb4 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -40,6 +40,7 @@ examples/explanation_grids.md examples/tutorial_nemo.ipynb examples/tutorial_croco_3D.ipynb examples/tutorial_mitgcm.ipynb +examples/tutorial_swash.ipynb examples/tutorial_fesom.ipynb examples/tutorial_schism.ipynb examples/tutorial_velocityconversion.ipynb From 106e926dd612aa25d42f70dcb07d3bf78ff88914 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 11:48:03 +0200 Subject: [PATCH 07/15] Add text to swash notebook --- docs/user_guide/examples/tutorial_swash.ipynb | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index b7905ddb1..a34fdf04c 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -8,10 +8,18 @@ "# 🖥️ SWASH tutorial" ] }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "This tutorial shows how to load in native Matlab files from the SWASH model and convert them to a Parcels-compatible `FieldSet`. The tutorial also shows how to run a simple particle tracking simulation using the SWASH data." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "1", + "id": "2", "metadata": {}, "outputs": [], "source": [ @@ -25,31 +33,50 @@ { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", "metadata": {}, "outputs": [], "source": [ - "swash_files = parcels.tutorial.open_dataset(\"SWASH_data/data\", download_only=True)" + "data_file, coord_file = parcels.tutorial.open_dataset(\n", + " \"SWASH_data/data\", download_only=True\n", + ")\n", + "print(data_file, coord_file)" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "Unlike the other tutorials, this tutorial uses the original SWASH output files in Matlab format. If you want to use your own SWASH output files, you should use the `GRD.mat` and `ALL.mat` files in the `parcels.convert.swash_to_sgrid()` function below." ] }, { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ "ds = parcels.convert.swash_to_sgrid(\n", - " data_file=swash_files[0], coord_file=swash_files[1], total_depth=8.0\n", + " data_file=data_file, coord_file=coord_file, total_depth=8.0\n", ")\n", "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", "fieldset.describe()" ] }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "Now, we can use this `FieldSet` to run a simulation (note it's very short because the dataset provided in this tutorial is only 20 seconds long)." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -73,10 +100,18 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "And then plot the results of the simulation, along with the water level at a given time step. The starting positions of the particles are also shown in white." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -90,12 +125,6 @@ "ax.set_xlim([14, 16])\n", "plt.show()" ] - }, - { - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [] } ], "metadata": { From 8e31d4f289b13b2f93f96db2e34ffcc79c858a97 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 12:00:27 +0200 Subject: [PATCH 08/15] Fix download_only also in remote/Zarr --- src/parcels/_datasets/remote.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parcels/_datasets/remote.py b/src/parcels/_datasets/remote.py index 14b9a390b..82d768d0f 100644 --- a/src/parcels/_datasets/remote.py +++ b/src/parcels/_datasets/remote.py @@ -186,8 +186,10 @@ def __init__(self, pup, path_relative_to_pup, zarr_format: Literal[2, 3] = 3): self.path_relative_to_root = path_relative_to_pup self.zarr_format = zarr_format - def open_dataset(self) -> xr.Dataset: + def open_dataset(self, download_only=False) -> xr.Dataset: self.pup.fetch(self.path_relative_to_root) + if download_only: + raise ValueError("download_only is not supported for zarr datasets.") return xr.open_zarr(ZipStore(Path(self.pup.path) / self.path_relative_to_root), zarr_format=self.zarr_format) From ccde7e252a85a44ac5dd130bd7c45427342b998f Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 12:01:12 +0200 Subject: [PATCH 09/15] Adding unit test for convert_swash --- tests/test_convert.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_convert.py b/tests/test_convert.py index 6a05bc960..f17cdd81e 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -174,6 +174,13 @@ def test_convert_copernicusmarine_no_logs(ds, caplog): assert caplog.text == "" +def test_convert_swash(): + data_file, coord_file = parcels.tutorial.open_dataset("SWASH_data/data", download_only=True) + + ds_fset = convert.swash_to_sgrid(data_file=data_file, coord_file=coord_file, total_depth=8.0) + FieldSet.from_sgrid_conventions(ds_fset) + + def test_convert_fesom_to_ugrid(): grid_file = open_remote_dataset("Benchmarks_FESOM2-baroclinic-gyre/grid") data_files = open_remote_dataset("Benchmarks_FESOM2-baroclinic-gyre/data") From 004cd9801e8181338c442280ae796ffd8fcb0e12 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 13:07:19 +0200 Subject: [PATCH 10/15] Cleaning up SWASH convert --- src/parcels/convert.py | 94 +++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/src/parcels/convert.py b/src/parcels/convert.py index 3703ef0b1..4bc958ed4 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -13,11 +13,13 @@ from __future__ import annotations import enum +import re import typing import warnings from typing import cast import numpy as np +import scipy.io as sio import xarray as xr import parcels._sgrid as sgrid @@ -567,16 +569,23 @@ def copernicusmarine_to_sgrid( return ds -def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: - """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files.""" - import re +def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Dataset: + """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files. - import pandas as pd - import scipy.io as sio - - import parcels._sgrid as sgrid + Parameters + ---------- + data_file : str + Path to the SWASH data file (MATLAB binary format). + coord_file : str + Path to the SWASH coordinate file (MATLAB binary format). + total_depth : float + Total depth of the water column. - ## First load the coordinates and data files (SWASH output - matlab binary format) + Returns + ------- + xarray.Dataset + Dataset object following SGRID conventions to be (optionally) modified and passed to a FieldSet constructor. + """ coord = sio.loadmat(coord_file) x = coord["Xp"][0, :] y = coord["Yp"][:, 0] @@ -588,17 +597,18 @@ def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: time_keys = sorted( set((int(m.group(1)), int(m.group(2))) for k in keys for m in [re.search(r"_(\d{6})_(\d{3})$", k)] if m) ) - times = [t[0] + t[1] / 1000 for t in time_keys] + times = np.array([t[0] * 1000 + t[1] for t in time_keys]).astype("timedelta64[ms]") n_layers = max(int(re.search(r"Vksi_k(\d+)_", k).group(1)) for k in keys if re.search(r"Vksi_k(\d+)_", k)) - n_w_layers = n_layers - - nx, ny, nt = len(x), len(y), len(times) + n_layers_f = n_layers + 1 + depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) + depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) + nt, ny, nx = len(times), len(y), len(x) watlev = np.full((nt, ny, nx), np.nan, dtype=np.float32) vksi = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) veta = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) - w = np.full((nt, n_w_layers, ny, nx), np.nan, dtype=np.float32) + w = np.full((nt, n_layers_f, ny, nx), np.nan, dtype=np.float32) for ti, (ts_int, ts_dec) in enumerate(time_keys): ts_str = f"{ts_int:06d}_{ts_dec:03d}" @@ -612,57 +622,29 @@ def swash_to_sgrid(coord_file, data_file, total_depth) -> xr.Dataset: if m: veta[ti, int(m.group(1)) - 1, :, :] = mat[k] m = re.match(rf"w(\d+)_{ts_str}$", k) - if m and int(m.group(1)) < n_w_layers: + if m and int(m.group(1)) < n_layers: w[ti, int(m.group(1)), :, :] = mat[k] - t0 = pd.Timestamp("2026-06-01 00:00:00") - time_dt = np.array([t0 + pd.to_timedelta(t, unit="s") for t in times], dtype="datetime64[ns]") - ds = xr.Dataset( { - "watlev": (["time", "y", "x"], watlev), - "U": (["time", "depth", "y", "x"], vksi), - "V": (["time", "depth", "y", "x"], veta), - "W": (["time", "depth_f", "y", "x"], w), - "botlev": (["y", "x"], bot), + "watlev": (["time", "lat", "lon"], watlev), + "U": (["time", "depth", "lat", "lon"], vksi), + "V": (["time", "depth", "lat", "lon"], veta), + "W": (["time", "depth_f", "lat", "lon"], w), + "botlev": (["lat", "lon"], bot), }, coords={ - "time": time_dt, - "depth": np.arange(1, n_layers + 1), - "depth_f": np.arange(0, n_w_layers), - "y": y, - "x": x, + "time": (["time"], times, {"axis": "T", "units": "ms"}), + "depth": (["depth"], depth_centers, {"axis": "Z", "units": "m", "positive": "down"}), + "depth_f": (["depth_f"], depth_interfaces, {"axis": "Z", "units": "m", "positive": "down"}), + "lat": (["lat"], y, {"axis": "Y", "units": "m"}), + "lon": (["lon"], x, {"axis": "X", "units": "m"}), }, ) - ds.attrs.update(source="SWASH version 11.01ABC", project="progWave", run="A14", Conventions="CF-1.8") - - n_layers = ds.sizes["depth"] - n_layers_f = ds.sizes["depth_f"] - if n_layers_f != n_layers: - raise ValueError(f"Expected depth_f to match depth in length (got {n_layers_f} vs {n_layers})") - - depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) - depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) - - # Rename x/y -> lon/lat: Parcels' from_sgrid_conventions expects these names - # literally, even on a flat/Cartesian mesh (units stay in meters). - ds = ds.rename({"x": "lon", "y": "lat"}) - - ds = ds.assign_coords( - { - "depth": ("depth", depth_centers), - "depth_f": ("depth_f", depth_interfaces), - } - ) - - ds["time"].attrs.update(axis="T") - ds["lon"].attrs.update(axis="X", units="m") - ds["lat"].attrs.update(axis="Y", units="m") - ds["depth"].attrs.update(axis="Z", units="m", positive="down") - ds["depth_f"].attrs.update(axis="Z", units="m", positive="down") - - if "grid" in ds.cf.cf_roles: - raise ValueError("Dataset already has a 'grid' variable (cf_role grid_topology).") + header = mat["__header__"] + if isinstance(header, bytes): + header = header.decode("utf-8") + ds.attrs.update(header=header, version=mat["__version__"], globals=mat["__globals__"]) ds["grid"] = xr.DataArray( 0, From 31f918006c3054f8b46736f0f0b9062c796c203e Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 13:20:53 +0200 Subject: [PATCH 11/15] Separating dowload_only into new get_dataset_files call --- docs/user_guide/examples/tutorial_swash.ipynb | 4 +- src/parcels/_datasets/remote.py | 45 +++++++++++++++---- src/parcels/tutorial.py | 24 +++++++++- tests/test_convert.py | 2 +- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index a34fdf04c..9b3685060 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -37,9 +37,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_file, coord_file = parcels.tutorial.open_dataset(\n", - " \"SWASH_data/data\", download_only=True\n", - ")\n", + "data_file, coord_file = parcels.tutorial.get_dataset_files(\"SWASH_data/data\")\n", "print(data_file, coord_file)" ] }, diff --git a/src/parcels/_datasets/remote.py b/src/parcels/_datasets/remote.py index 82d768d0f..194f626ad 100644 --- a/src/parcels/_datasets/remote.py +++ b/src/parcels/_datasets/remote.py @@ -151,12 +151,13 @@ def __init__(self, pup: pooch.Pooch, path_relative_to_pup: str, pre_decode_cf_ca first, second, *_ = path_relative_to_pup.split("/") self.v3_dataset_name = f"{first}/{second}" # e.g., data/my_dataset - def open_dataset(self, download_only=False) -> xr.Dataset: + def get_dataset_files(self) -> list[str]: self.download_relevant_files() - if download_only: - matches = sorted(glob.glob(f"{self.pup.path}/{self.path_relative_to_root}")) - return matches if len(matches) != 1 else matches[0] + matches = sorted(glob.glob(f"{self.pup.path}/{self.path_relative_to_root}")) + return matches + def open_dataset(self) -> xr.Dataset: + self.download_relevant_files() with xr.set_options(use_new_combine_kwarg_defaults=True): ds = xr.open_mfdataset( f"{self.pup.path}/{self.path_relative_to_root}", @@ -186,10 +187,8 @@ def __init__(self, pup, path_relative_to_pup, zarr_format: Literal[2, 3] = 3): self.path_relative_to_root = path_relative_to_pup self.zarr_format = zarr_format - def open_dataset(self, download_only=False) -> xr.Dataset: + def open_dataset(self) -> xr.Dataset: self.pup.fetch(self.path_relative_to_root) - if download_only: - raise ValueError("download_only is not supported for zarr datasets.") return xr.open_zarr(ZipStore(Path(self.pup.path) / self.path_relative_to_root), zarr_format=self.zarr_format) @@ -288,7 +287,7 @@ def list_remote_datasets(purpose: _TPurpose | Literal["any"] = "any") -> list[st return [k for (k, (_, p)) in _DATASET_KEYS_AND_CONFIGS.items() if p == purpose_enum] -def open_remote_dataset(name: str, purpose: _TPurpose | Literal["any"] = "any", download_only=False): +def open_remote_dataset(name: str, purpose: _TPurpose | Literal["any"] = "any"): """Download and open a remote dataset as an :class:`xarray.Dataset`. Use :func:`list_datasets` to see the available dataset names. @@ -313,4 +312,32 @@ def open_remote_dataset(name: str, purpose: _TPurpose | Literal["any"] = "any", ) dataset_config = _DATASET_KEYS_AND_CONFIGS[name][0] - return dataset_config.open_dataset(download_only=download_only) + return dataset_config.open_dataset() + + +def get_remote_dataset(name: str, purpose: _TPurpose | Literal["any"] = "any") -> list[str]: + """Download the files of a remote dataset. + + Use :func:`list_datasets` to see the available dataset names. + + Parameters + ---------- + name : str + Name of the dataset to open. Must be one of the keys returned by + :func:`list_datasets`. + purpose : {'any', 'testing', 'tutorial'}, optional + Purpose filter used to populate the error message when ``name`` is not + found. Defaults to ``'any'``. + + Returns + ------- + list of str + The list of dataset files. + """ + if name not in list_remote_datasets(purpose=purpose): + raise ValueError( + f"Dataset {name!r} not found. Available datasets are: " + ", ".join(list_remote_datasets(purpose=purpose)) + ) + + dataset_config = _DATASET_KEYS_AND_CONFIGS[name][0] + return dataset_config.get_dataset_files() diff --git a/src/parcels/tutorial.py b/src/parcels/tutorial.py index 99d4f3df6..7a5466edf 100644 --- a/src/parcels/tutorial.py +++ b/src/parcels/tutorial.py @@ -1,3 +1,4 @@ +from parcels._datasets.remote import get_remote_dataset as _get_remote_dataset from parcels._datasets.remote import list_remote_datasets as _list_remote_datasets from parcels._datasets.remote import open_remote_dataset as _open_remote_dataset @@ -17,7 +18,7 @@ def list_datasets() -> list[str]: return _list_remote_datasets(purpose="tutorial") -def open_dataset(name: str, download_only=False): +def open_dataset(name: str): """Download and open a tutorial dataset as an :class:`xarray.Dataset`. Use :func:`list_datasets` to see the available dataset names. @@ -33,4 +34,23 @@ def open_dataset(name: str, download_only=False): xarray.Dataset The requested dataset. """ - return _open_remote_dataset(name, purpose="tutorial", download_only=download_only) + return _open_remote_dataset(name, purpose="tutorial") + + +def get_dataset_files(name: str): + """Download the files of a tutorial dataset. + + Use :func:`list_datasets` to see the available dataset names. + + Parameters + ---------- + name : str + Name of the dataset to open. Must be one of the keys returned by + :func:`list_datasets`. + + Returns + ------- + list of str + The list of dataset files. + """ + return _get_remote_dataset(name, purpose="tutorial") diff --git a/tests/test_convert.py b/tests/test_convert.py index f17cdd81e..8486420fb 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -175,7 +175,7 @@ def test_convert_copernicusmarine_no_logs(ds, caplog): def test_convert_swash(): - data_file, coord_file = parcels.tutorial.open_dataset("SWASH_data/data", download_only=True) + data_file, coord_file = parcels.tutorial.get_dataset_files("SWASH_data/data") ds_fset = convert.swash_to_sgrid(data_file=data_file, coord_file=coord_file, total_depth=8.0) FieldSet.from_sgrid_conventions(ds_fset) From fd6396142a30e329a7faf48091a0354a26b49aca Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 15:50:18 +0200 Subject: [PATCH 12/15] Adding get_dataset_files for other classes --- src/parcels/_datasets/remote.py | 8 ++++++++ src/parcels/convert.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/parcels/_datasets/remote.py b/src/parcels/_datasets/remote.py index 194f626ad..b62003621 100644 --- a/src/parcels/_datasets/remote.py +++ b/src/parcels/_datasets/remote.py @@ -136,6 +136,9 @@ def _get_data_home() -> Path: class _ParcelsDataset(abc.ABC): + @abc.abstractmethod + def get_dataset_files(self) -> list[str]: ... + @abc.abstractmethod def open_dataset(self) -> xr.Dataset: ... @@ -187,6 +190,11 @@ def __init__(self, pup, path_relative_to_pup, zarr_format: Literal[2, 3] = 3): self.path_relative_to_root = path_relative_to_pup self.zarr_format = zarr_format + def get_dataset_files(self) -> list[str]: + raise NotImplementedError( + "get_dataset_files is not supported for zipped zarr datasets. Use open_dataset instead." + ) + def open_dataset(self) -> xr.Dataset: self.pup.fetch(self.path_relative_to_root) return xr.open_zarr(ZipStore(Path(self.pup.path) / self.path_relative_to_root), zarr_format=self.zarr_format) diff --git a/src/parcels/convert.py b/src/parcels/convert.py index 4bc958ed4..6b8a25ba8 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -599,7 +599,11 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da ) times = np.array([t[0] * 1000 + t[1] for t in time_keys]).astype("timedelta64[ms]") - n_layers = max(int(re.search(r"Vksi_k(\d+)_", k).group(1)) for k in keys if re.search(r"Vksi_k(\d+)_", k)) + layer_indices = [] + for key in keys: + match = re.search(r"Vksi_k(\d+)_", key) + layer_indices.append(int(match.group(1))) + n_layers = max(layer_indices) n_layers_f = n_layers + 1 depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) From 38dceec67a85bd5bdfd299f7404404554b67099f Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 16:11:56 +0200 Subject: [PATCH 13/15] Using C-Grid(?) implementation of SWASH --- docs/user_guide/examples/tutorial_swash.ipynb | 2 +- src/parcels/convert.py | 38 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index 9b3685060..467f89868 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -116,7 +116,7 @@ "df = parcels.read_particlefile(\"output-swash.parquet\")\n", "\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", - "waterlevel = ds.isel(time=2).watlev.plot(cmap=\"magma\", ax=ax)\n", + "waterlevel = ds.isel(time=2).watlev.plot(x=\"lon\", y=\"lat\", cmap=\"magma\", ax=ax)\n", "for traj in df.partition_by(\"particle_id\"):\n", " ax.plot(traj[\"x\"][0], traj[\"y\"][0], \"wo\", markersize=5)\n", " ax.plot(traj[\"x\"], traj[\"y\"], color=\"k\")\n", diff --git a/src/parcels/convert.py b/src/parcels/convert.py index 6b8a25ba8..752943be2 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -587,8 +587,10 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da Dataset object following SGRID conventions to be (optionally) modified and passed to a FieldSet constructor. """ coord = sio.loadmat(coord_file) - x = coord["Xp"][0, :] - y = coord["Yp"][:, 0] + lon = coord["Xp"] + lat = coord["Yp"] + XG = np.arange(lon.shape[1]) + YG = np.arange(lat.shape[0]) bot = coord["Botlev"] mat = sio.loadmat(data_file) @@ -599,16 +601,12 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da ) times = np.array([t[0] * 1000 + t[1] for t in time_keys]).astype("timedelta64[ms]") - layer_indices = [] - for key in keys: - match = re.search(r"Vksi_k(\d+)_", key) - layer_indices.append(int(match.group(1))) - n_layers = max(layer_indices) + n_layers = len(set([k.split("_")[1] for k in keys if "Vksi" in k])) n_layers_f = n_layers + 1 depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) - nt, ny, nx = len(times), len(y), len(x) + nt, ny, nx = len(times), len(YG), len(XG) watlev = np.full((nt, ny, nx), np.nan, dtype=np.float32) vksi = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) veta = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) @@ -631,18 +629,22 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da ds = xr.Dataset( { - "watlev": (["time", "lat", "lon"], watlev), - "U": (["time", "depth", "lat", "lon"], vksi), - "V": (["time", "depth", "lat", "lon"], veta), - "W": (["time", "depth_f", "lat", "lon"], w), - "botlev": (["lat", "lon"], bot), + "watlev": (["time", "YG", "XG"], watlev), + "U": (["time", "depth", "YC", "XC"], vksi), + "V": (["time", "depth", "YC", "XC"], veta), + "W": (["time", "depth_f", "YC", "XC"], w), + "botlev": (["YC", "XC"], bot), }, coords={ "time": (["time"], times, {"axis": "T", "units": "ms"}), "depth": (["depth"], depth_centers, {"axis": "Z", "units": "m", "positive": "down"}), "depth_f": (["depth_f"], depth_interfaces, {"axis": "Z", "units": "m", "positive": "down"}), - "lat": (["lat"], y, {"axis": "Y", "units": "m"}), - "lon": (["lon"], x, {"axis": "X", "units": "m"}), + "YG": (["YG"], YG, {"axis": "Y"}), + "YC": (["YC"], YG - 0.5, {"axis": "Y", "c_grid_axis_shift": +0.5}), + "XG": (["XG"], XG, {"axis": "X"}), + "XC": (["XC"], XG - 0.5, {"axis": "X", "c_grid_axis_shift": +0.5}), + "lat": (["YG", "XG"], lat, {"axis": "Y", "units": "m"}), + "lon": (["YG", "XG"], lon, {"axis": "X", "units": "m"}), }, ) header = mat["__header__"] @@ -655,11 +657,11 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da attrs=sgrid.SGrid2DMetadata( cf_role="grid_topology", topology_dimension=2, - node_dimensions=("lon", "lat"), + node_dimensions=("XG", "YG"), node_coordinates=("lon", "lat"), face_dimensions=( - sgrid.FaceNodePadding("lon", "lon", sgrid.Padding.NONE), - sgrid.FaceNodePadding("lat", "lat", sgrid.Padding.NONE), + sgrid.FaceNodePadding("XC", "XG", sgrid.Padding.LOW), + sgrid.FaceNodePadding("YC", "YG", sgrid.Padding.LOW), ), vertical_dimensions=(sgrid.FaceNodePadding("depth", "depth_f", sgrid.Padding.LOW),), ).to_attrs(), From 974c02e67a097ae4620de2d9c383e3be577f9404 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 16:52:43 +0200 Subject: [PATCH 14/15] Demoving total_depth - and updating C-grid definition in SWASH --- docs/user_guide/examples/tutorial_swash.ipynb | 4 +- src/parcels/convert.py | 53 +++++++++---------- tests/test_convert.py | 2 +- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/docs/user_guide/examples/tutorial_swash.ipynb b/docs/user_guide/examples/tutorial_swash.ipynb index 467f89868..c23e59834 100644 --- a/docs/user_guide/examples/tutorial_swash.ipynb +++ b/docs/user_guide/examples/tutorial_swash.ipynb @@ -56,9 +56,7 @@ "metadata": {}, "outputs": [], "source": [ - "ds = parcels.convert.swash_to_sgrid(\n", - " data_file=data_file, coord_file=coord_file, total_depth=8.0\n", - ")\n", + "ds = parcels.convert.swash_to_sgrid(data_file=data_file, coord_file=coord_file)\n", "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", "fieldset.describe()" ] diff --git a/src/parcels/convert.py b/src/parcels/convert.py index 752943be2..d5b32f971 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -569,7 +569,7 @@ def copernicusmarine_to_sgrid( return ds -def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Dataset: +def swash_to_sgrid(data_file: str, coord_file: str) -> xr.Dataset: """Create an sgrid-compliant xarray.Dataset from a dataset of SWASH netcdf files. Parameters @@ -578,8 +578,6 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da Path to the SWASH data file (MATLAB binary format). coord_file : str Path to the SWASH coordinate file (MATLAB binary format). - total_depth : float - Total depth of the water column. Returns ------- @@ -589,8 +587,8 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da coord = sio.loadmat(coord_file) lon = coord["Xp"] lat = coord["Yp"] - XG = np.arange(lon.shape[1]) - YG = np.arange(lat.shape[0]) + XC = np.arange(lon.shape[1]) + YC = np.arange(lat.shape[0]) bot = coord["Botlev"] mat = sio.loadmat(data_file) @@ -601,16 +599,15 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da ) times = np.array([t[0] * 1000 + t[1] for t in time_keys]).astype("timedelta64[ms]") - n_layers = len(set([k.split("_")[1] for k in keys if "Vksi" in k])) - n_layers_f = n_layers + 1 - depth_centers = np.array([total_depth * (i - 0.5) / n_layers for i in range(1, n_layers + 1)], dtype=np.float32) - depth_interfaces = np.array([total_depth * i / n_layers for i in range(n_layers_f)], dtype=np.float32) + nz = len(set([k.split("_")[1] for k in keys if "Vksi" in k])) + depth_centers = np.linspace(1.0 / (2 * nz), 1.0 - 1.0 / (2 * nz), nz) + depth_interfaces = np.linspace(0, 1, nz + 1) - nt, ny, nx = len(times), len(YG), len(XG) + nt, ny, nx = len(times), len(YC), len(XC) watlev = np.full((nt, ny, nx), np.nan, dtype=np.float32) - vksi = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) - veta = np.full((nt, n_layers, ny, nx), np.nan, dtype=np.float32) - w = np.full((nt, n_layers_f, ny, nx), np.nan, dtype=np.float32) + vksi = np.full((nt, nz, ny, nx), np.nan, dtype=np.float32) + veta = np.full((nt, nz, ny, nx), np.nan, dtype=np.float32) + w = np.full((nt, nz + 1, ny, nx), np.nan, dtype=np.float32) for ti, (ts_int, ts_dec) in enumerate(time_keys): ts_str = f"{ts_int:06d}_{ts_dec:03d}" @@ -624,27 +621,27 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da if m: veta[ti, int(m.group(1)) - 1, :, :] = mat[k] m = re.match(rf"w(\d+)_{ts_str}$", k) - if m and int(m.group(1)) < n_layers: + if m and int(m.group(1)) < nz: w[ti, int(m.group(1)), :, :] = mat[k] ds = xr.Dataset( { "watlev": (["time", "YG", "XG"], watlev), - "U": (["time", "depth", "YC", "XC"], vksi), - "V": (["time", "depth", "YC", "XC"], veta), + "U": (["time", "depth", "YC", "XG"], vksi), + "V": (["time", "depth", "YG", "XC"], veta), "W": (["time", "depth_f", "YC", "XC"], w), - "botlev": (["YC", "XC"], bot), + "botlev": (["YG", "XG"], bot), }, coords={ "time": (["time"], times, {"axis": "T", "units": "ms"}), - "depth": (["depth"], depth_centers, {"axis": "Z", "units": "m", "positive": "down"}), - "depth_f": (["depth_f"], depth_interfaces, {"axis": "Z", "units": "m", "positive": "down"}), - "YG": (["YG"], YG, {"axis": "Y"}), - "YC": (["YC"], YG - 0.5, {"axis": "Y", "c_grid_axis_shift": +0.5}), - "XG": (["XG"], XG, {"axis": "X"}), - "XC": (["XC"], XG - 0.5, {"axis": "X", "c_grid_axis_shift": +0.5}), - "lat": (["YG", "XG"], lat, {"axis": "Y", "units": "m"}), - "lon": (["YG", "XG"], lon, {"axis": "X", "units": "m"}), + "depth": (["depth"], depth_centers, {"axis": "Z", "units": "normalised", "positive": "down"}), + "depth_f": (["depth_f"], depth_interfaces, {"axis": "Z", "units": "normalised", "positive": "down"}), + "YG": (["YG"], YC + 0.5, {"axis": "Y", "c_grid_axis_shift": +0.5}), + "YC": (["YC"], YC, {"axis": "Y"}), + "XG": (["XG"], XC + 0.5, {"axis": "X", "c_grid_axis_shift": +0.5}), + "XC": (["XC"], XC, {"axis": "X"}), + "lat": (["YG", "XG"], lat, {"axis": "Y", "units": "m", "c_grid_axis_shift": +0.5}), + "lon": (["YG", "XG"], lon, {"axis": "X", "units": "m", "c_grid_axis_shift": +0.5}), }, ) header = mat["__header__"] @@ -657,11 +654,11 @@ def swash_to_sgrid(data_file: str, coord_file: str, total_depth: float) -> xr.Da attrs=sgrid.SGrid2DMetadata( cf_role="grid_topology", topology_dimension=2, - node_dimensions=("XG", "YG"), + node_dimensions=("XC", "YC"), node_coordinates=("lon", "lat"), face_dimensions=( - sgrid.FaceNodePadding("XC", "XG", sgrid.Padding.LOW), - sgrid.FaceNodePadding("YC", "YG", sgrid.Padding.LOW), + sgrid.FaceNodePadding("XC", "XG", sgrid.Padding.HIGH), + sgrid.FaceNodePadding("YC", "YG", sgrid.Padding.HIGH), ), vertical_dimensions=(sgrid.FaceNodePadding("depth", "depth_f", sgrid.Padding.LOW),), ).to_attrs(), diff --git a/tests/test_convert.py b/tests/test_convert.py index 8486420fb..ad82a63fb 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -177,7 +177,7 @@ def test_convert_copernicusmarine_no_logs(ds, caplog): def test_convert_swash(): data_file, coord_file = parcels.tutorial.get_dataset_files("SWASH_data/data") - ds_fset = convert.swash_to_sgrid(data_file=data_file, coord_file=coord_file, total_depth=8.0) + ds_fset = convert.swash_to_sgrid(data_file=data_file, coord_file=coord_file) FieldSet.from_sgrid_conventions(ds_fset) From 6dab2f3b3bab1910db618f36b6385abb85fdae81 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 23 Jul 2026 17:12:17 +0200 Subject: [PATCH 15/15] Adding warning about experimental nature of swash_to_sgrid --- src/parcels/convert.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/parcels/convert.py b/src/parcels/convert.py index d5b32f971..3fe7b36ec 100644 --- a/src/parcels/convert.py +++ b/src/parcels/convert.py @@ -584,6 +584,13 @@ def swash_to_sgrid(data_file: str, coord_file: str) -> xr.Dataset: xarray.Dataset Dataset object following SGRID conventions to be (optionally) modified and passed to a FieldSet constructor. """ + warnings.warn( + "The swash_to_sgrid function is experimental and may not work for all SWASH datasets. " + "Furthermore, we are not entirely confident that the SGrid layout for SWASH is implemented correctly. " + "Please report any issues to the Parcels GitHub repository.", + UserWarning, + stacklevel=2, + ) coord = sio.loadmat(coord_file) lon = coord["Xp"] lat = coord["Yp"] @@ -624,6 +631,7 @@ def swash_to_sgrid(data_file: str, coord_file: str) -> xr.Dataset: if m and int(m.group(1)) < nz: w[ti, int(m.group(1)), :, :] = mat[k] + # TODO double-check that the C-grid definition for SWASH here is correct ds = xr.Dataset( { "watlev": (["time", "YG", "XG"], watlev),