diff --git a/docs/user-guide/area_calc.ipynb b/docs/user-guide/area_calc.ipynb index 49e4d1ace..03db24406 100644 --- a/docs/user-guide/area_calc.ipynb +++ b/docs/user-guide/area_calc.ipynb @@ -22,19 +22,184 @@ "2. Options for `Grid.calculate_total_face_area` Function\n", "3. Getting Area of Individual Faces\n", "4. Calculate Area of a Single Triangle in Cartesian Coordinates\n", - "5. Calculate Area from Multiple Faces in Spherical Coordinates" + "5. Calculate Area from Multiple Faces in Spherical Coordinates\n", + "6. Demonstrate Area Correction at Line of Constant Lattitude" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\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 comm.open();\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": "31b4155a-9f7f-403c-ba42-727a15e63f20" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\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 comm.open();\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" + } + ], "source": [ "import uxarray as ux\n", "import numpy as np" @@ -49,14 +214,434 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<uxarray.Grid>\n",
+       "Original Grid Type: UGRID\n",
+       "Grid Dimensions:\n",
+       "  * n_node: 5402\n",
+       "  * n_face: 5400\n",
+       "  * n_max_face_nodes: 4\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "Grid Coordinates (Spherical):\n",
+       "  * node_lon: (5402,)\n",
+       "  * node_lat: (5402,)\n",
+       "Grid Coordinates (Cartesian):\n",
+       "Grid Connectivity Variables:\n",
+       "  * face_node_connectivity: (5400, 4)\n",
+       "Grid Descriptor Variables:\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "
" + ], + "text/plain": [ + "\n", + "Original Grid Type: UGRID\n", + "Grid Dimensions:\n", + " * n_node: 5402\n", + " * n_face: 5400\n", + " * n_max_face_nodes: 4\n", + " * n_nodes_per_face: (5400,)\n", + "Grid Coordinates (Spherical):\n", + " * node_lon: (5402,)\n", + " * node_lat: (5402,)\n", + "Grid Coordinates (Cartesian):\n", + "Grid Connectivity Variables:\n", + " * face_node_connectivity: (5400, 4)\n", + "Grid Descriptor Variables:\n", + " * n_nodes_per_face: (5400,)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "base_path = \"../../test/meshfiles/\"\n", "grid_path = base_path + \"/ugrid/outCSne30/outCSne30.ug\"\n", @@ -75,14 +660,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(12.566370614678554)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "t4_area = ugrid.calculate_total_face_area()\n", "t4_area" @@ -106,9 +702,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(12.571403993719983)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "t1_area = ugrid.calculate_total_face_area(quadrature_rule=\"triangular\", order=1)\n", "t1_area" @@ -139,14 +746,411 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray 'face_areas' (n_face: 5400)> Size: 43kB\n",
+       "array([0.00211174, 0.00211221, 0.00210723, ..., 0.00210723, 0.00211221,\n",
+       "       0.00211174])\n",
+       "Dimensions without coordinates: n_face\n",
+       "Attributes:\n",
+       "    cf_role:    face_areas\n",
+       "    long_name:  Area of each face.
" + ], + "text/plain": [ + " Size: 43kB\n", + "array([0.00211174, 0.00211221, 0.00210723, ..., 0.00210723, 0.00211221,\n", + " 0.00211174])\n", + "Dimensions without coordinates: n_face\n", + "Attributes:\n", + " cf_role: face_areas\n", + " long_name: Area of each face." + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "ugrid.face_areas" ] @@ -160,9 +1164,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(12.566370614359112)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "all_face_areas, all_face_jacobians = ugrid.compute_face_areas(\n", " quadrature_rule=\"gaussian\", order=4\n", @@ -180,9 +1195,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(np.float64(0.005033379360810386),\n", + " np.float64(3.1938185429680743e-10),\n", + " np.float64(6.039613253960852e-14))" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "actual_area = 4 * np.pi\n", "diff_t4_area = np.abs(t4_area - actual_area)\n", @@ -212,14 +1240,429 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<uxarray.Grid>\n",
+       "Original Grid Type: Face Vertices\n",
+       "Grid Dimensions:\n",
+       "  * n_node: 3\n",
+       "  * n_face: 1\n",
+       "  * n_max_face_nodes: 3\n",
+       "  * n_nodes_per_face: (1,)\n",
+       "Grid Coordinates (Spherical):\n",
+       "Grid Coordinates (Cartesian):\n",
+       "  * node_x: (3,)\n",
+       "  * node_y: (3,)\n",
+       "  * node_z: (3,)\n",
+       "Grid Connectivity Variables:\n",
+       "  * face_node_connectivity: (1, 3)\n",
+       "Grid Descriptor Variables:\n",
+       "  * n_nodes_per_face: (1,)\n",
+       "
" + ], + "text/plain": [ + "\n", + "Original Grid Type: Face Vertices\n", + "Grid Dimensions:\n", + " * n_node: 3\n", + " * n_face: 1\n", + " * n_max_face_nodes: 3\n", + " * n_nodes_per_face: (1,)\n", + "Grid Coordinates (Spherical):\n", + "Grid Coordinates (Cartesian):\n", + " * node_x: (3,)\n", + " * node_y: (3,)\n", + " * node_z: (3,)\n", + "Grid Connectivity Variables:\n", + " * face_node_connectivity: (1, 3)\n", + "Grid Descriptor Variables:\n", + " * n_nodes_per_face: (1,)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "verts = [\n", " [\n", @@ -237,14 +1680,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(1.0719419938548218)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "vgrid.calculate_total_face_area()" ] @@ -258,14 +1712,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(1.0475702709086991)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Calculate the area of the triangle\n", "area_gaussian = vgrid.calculate_total_face_area(quadrature_rule=\"gaussian\", order=5)\n", @@ -288,7 +1753,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "collapsed": false, "jupyter": { @@ -342,14 +1807,430 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<uxarray.Grid>\n",
+       "Original Grid Type: Face Vertices\n",
+       "Grid Dimensions:\n",
+       "  * n_node: 14\n",
+       "  * n_face: 3\n",
+       "  * n_max_face_nodes: 6\n",
+       "  * n_nodes_per_face: (3,)\n",
+       "Grid Coordinates (Spherical):\n",
+       "  * node_lon: (14,)\n",
+       "  * node_lat: (14,)\n",
+       "Grid Coordinates (Cartesian):\n",
+       "Grid Connectivity Variables:\n",
+       "  * face_node_connectivity: (3, 6)\n",
+       "Grid Descriptor Variables:\n",
+       "  * n_nodes_per_face: (3,)\n",
+       "
" + ], + "text/plain": [ + "\n", + "Original Grid Type: Face Vertices\n", + "Grid Dimensions:\n", + " * n_node: 14\n", + " * n_face: 3\n", + " * n_max_face_nodes: 6\n", + " * n_nodes_per_face: (3,)\n", + "Grid Coordinates (Spherical):\n", + " * node_lon: (14,)\n", + " * node_lat: (14,)\n", + "Grid Coordinates (Cartesian):\n", + "Grid Connectivity Variables:\n", + " * face_node_connectivity: (3, 6)\n", + "Grid Descriptor Variables:\n", + " * n_nodes_per_face: (3,)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "verts_grid = ux.open_grid(faces_verts_ndarray, latlon=True)\n", "\n", @@ -358,23 +2239,320 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.14323746, 0.25118746, 0.12141312])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "area, jacobian = verts_grid.compute_face_areas()\n", "area" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Area Correction\n", + "\n", + "The correction, \\(A\\), is calculated using the following formula:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{math}\n", + "A = 2arctan(z(x_{1}y_{2} - x_{2} y_{1}) / {x_{1}^2 + y_{1}^2 + x_{1} x_{2} + y_{1} y_{2}) - z (x_{1} y_{2} - x_{2} y_{1} / x_{1} x_{2} + y_{1} y_{2}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Where:\n", + "
    \n", + "\n", + "
  • (x1, y1, z) are the Cartesian coordinates of the first node.
  • \n", + "\n", + "
  • (x2, y2, z) are the Cartesian coordinates of the second node (note that the z coordinate is the same for both nodes).
  • \n", + "\n", + "
\n", + "\n", + "### Assumptions:\n", + "- This formula assumes that the input coordinates \\((x_1, y_1)\\) and \\((x_2, y_2)\\) are normalized (i.e., they lie on the unit sphere).\n", + "\n", + "\n", + "For the same large triangle used in **Section 4**, we calculate the area correction term when an edge lies along the line of constant latitude.\n", + "\n", + "### The following code:\n", + "- Creates a spherical rectangle and calculates the exact area using the spherical excess formula.\n", + "- Plots the rectangle, it has two lines of constant lattitude.\n", + "- A mesh is formed with uxarray and area is calculated\n", + "- Area is again calculated with `correct_area` flag set to `True`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The area of the spherical rectangle is approximately 1.132592 steradians\n", + "The area of the spherical rectangle is approximately 45971520.05 square kilometers\n" + ] + } + ], + "source": [ + "def theoretical_spherical_rectangle_area(lons, lats, radius=1):\n", + " \"\"\"\n", + " Calculate the area of a spherical rectangle on a sphere.\n", + "\n", + " Parameters:\n", + " lons (np.ndarray): Longitudes of the rectangle's corners (in degrees).\n", + " lats (np.ndarray): Latitudes of the rectangle's corners (in degrees).\n", + " radius (float): Radius of the sphere (default is unit sphere).\n", + "\n", + " Returns:\n", + " float: Area of the spherical rectangle in steradians (or square units if radius is provided).\n", + " \"\"\"\n", + " # Convert degrees to radians\n", + " lons_rad = np.radians(lons)\n", + " lats_rad = np.radians(lats)\n", + "\n", + " # Compute longitude and latitude differences\n", + " delta_lambda = abs(lons_rad[0] - lons_rad[2]) # Assuming rectangular shape\n", + " sin_phi_diff = abs(np.sin(lats_rad[0]) - np.sin(lats_rad[2]))\n", + "\n", + " # Calculate area\n", + " area = radius**2 * delta_lambda * sin_phi_diff\n", + " return area\n", + "\n", + "\n", + "# Define nodes\n", + "node_lon = np.array([90.0, 90.0, 10.0, 10.0]) # Longitudes in degrees\n", + "node_lat = np.array([80.0, 10.0, 10.0, 80.0]) # Latitudes in degrees\n", + "\n", + "# Calculate the area\n", + "area_steradians = theoretical_spherical_rectangle_area(node_lon, node_lat)\n", + "print(\n", + " f\"The area of the spherical rectangle is approximately {area_steradians:.6f} steradians\"\n", + ")\n", + "\n", + "# If Earth's radius is used (R ≈ 6371 km):\n", + "earth_radius_km = 6371\n", + "area_km2 = theoretical_spherical_rectangle_area(\n", + " node_lon, node_lat, radius=earth_radius_km\n", + ")\n", + "print(\n", + " f\"The area of the spherical rectangle is approximately {area_km2:.2f} square kilometers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Path [Longitude,Latitude]" + ] + }, + "execution_count": 10, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "6d081030-85b6-478a-b5f2-230275b30e6d" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "# Define face-node connectivity and create/plot a uxarray object face\n", + "face_node_connectivity = np.array([[0, 1, 2, 3]])\n", + "\n", + "face = ux.Grid.from_topology(\n", + " node_lon=node_lon,\n", + " node_lat=node_lat,\n", + " face_node_connectivity=face_node_connectivity,\n", + " fill_value=-1,\n", + ")\n", + "face.plot(backend=\"bokeh\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "area_no_correction = face.compute_face_areas(quadrature_rule=\"gaussian\", order=8)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Check if edge passes through pole: False\n", + "6.030208312509488e-17 0.984807753012208 0.9698463103929541 0.17101007166283433 0.17364817766693033 correction: 0.04692118879641144\n", + "\n", + "\n", + "calculated correction: 0.04692118879641144\n", + "Edge is in the Northern hemisphere.\n", + "For Node 1 [6.03020831e-17 9.84807753e-01 1.73648178e-01] \n", + " and Node 2 [0.96984631 0.17101007 0.17364818] \n", + "CORRECTION 0.04692118879641144\n", + "Check if edge passes through pole: False\n", + "0.1710100716628344 0.030153689607045817 1.0632884247878861e-17 0.17364817766693041 0.984807753012208 correction: 0.006156712978357515\n", + "\n", + "\n", + "calculated correction: 0.006156712978357515\n", + "Edge is in the Northern hemisphere.\n", + "For Node 1 [0.17101007 0.03015369 0.98480775] \n", + " and Node 2 [1.06328842e-17 1.73648178e-01 9.84807753e-01] \n", + "CORRECTION -0.006156712978357515\n", + "AREA Before Correction 1.0918281804990866\n", + "AREA After Correction 1.1325926563171405\n" + ] + } + ], + "source": [ + "corrected_area = face.compute_face_areas(\n", + " quadrature_rule=\"gaussian\", order=8, correct_area=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The percentage difference between the corrected and uncorrected areas is 3.73%\n" + ] + } + ], + "source": [ + "# Calculate the percentage difference between the corrected and uncorrected areas\n", + "percentage_difference = (\n", + " (corrected_area[0][0] - area_no_correction[0][0]) / area_no_correction[0][0] * 100\n", + ")\n", + "\n", + "print(\n", + " f\"The percentage difference between the corrected and uncorrected areas is {percentage_difference:.2f}%\"\n", + ")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "uxarray_env3.12", "language": "python", "name": "python3" }, @@ -388,7 +2566,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/test/constants.py b/test/constants.py index 4a54e1b1a..eeb23ecf3 100644 --- a/test/constants.py +++ b/test/constants.py @@ -7,6 +7,7 @@ NNODES_outRLL1deg = 64442 DATAVARS_outCSne30 = 4 TRI_AREA = 1.047 +CORRECTED_TRI_AREA = 1.3281 # 4*Pi is 12.56 MESH30_AREA = 12.566 PSI_INTG = 12.566 diff --git a/test/test_grid.py b/test/test_grid.py index 4c8d98c76..8300b7590 100644 --- a/test/test_grid.py +++ b/test/test_grid.py @@ -266,15 +266,16 @@ def test_face_areas_calculate_total_face_area_triangle(): # validate the grid assert grid_verts.validate() - # calculate area - area_gaussian = grid_verts.calculate_total_face_area( - quadrature_rule="gaussian", order=5) - nt.assert_almost_equal(area_gaussian, constants.TRI_AREA, decimal=3) - + # calculate area without correction area_triangular = grid_verts.calculate_total_face_area( quadrature_rule="triangular", order=4) nt.assert_almost_equal(area_triangular, constants.TRI_AREA, decimal=1) + # calculate area + area_gaussian = grid_verts.calculate_total_face_area( + quadrature_rule="gaussian", order=5, correct_area=True) + nt.assert_almost_equal(area_gaussian, constants.CORRECTED_TRI_AREA, decimal=3) + def test_face_areas_calculate_total_face_area_file(): """Create a uxarray grid from vertices and saves an exodus file.""" area = ux.open_grid(gridfile_CSne30).calculate_total_face_area() diff --git a/uxarray/grid/area.py b/uxarray/grid/area.py index b785f87f8..ef7073868 100644 --- a/uxarray/grid/area.py +++ b/uxarray/grid/area.py @@ -7,7 +7,13 @@ @njit(cache=True) def calculate_face_area( - x, y, z, quadrature_rule="gaussian", order=4, coords_type="spherical" + x, + y, + z, + quadrature_rule="gaussian", + order=4, + coords_type="spherical", + correct_area=False, ): """Calculate area of a face on sphere. @@ -56,23 +62,30 @@ def calculate_face_area( # num triangles is two less than the total number of nodes num_triangles = num_nodes - 2 + if coords_type == "spherical": + # Preallocate arrays for Cartesian coordinates + n_points = len(x) + x_cartesian = np.empty(n_points) + y_cartesian = np.empty(n_points) + z_cartesian = np.empty(n_points) + + # Convert all points to Cartesian coordinates using an explicit loop + for i in range(n_points): + lon_rad = np.deg2rad(x[i]) + lat_rad = np.deg2rad(y[i]) + cartesian = _lonlat_rad_to_xyz(lon_rad, lat_rad) + x_cartesian[i], y_cartesian[i], z_cartesian[i] = cartesian + + x, y, z = x_cartesian, y_cartesian, z_cartesian + # Using tempestremap GridElements: https://github.com/ClimateGlobalChange/tempestremap/blob/master/src/GridElements.cpp # loop through all sub-triangles of face + total_correction = 0.0 for j in range(0, num_triangles): node1 = np.array([x[0], y[0], z[0]], dtype=x.dtype) node2 = np.array([x[j + 1], y[j + 1], z[j + 1]], dtype=x.dtype) node3 = np.array([x[j + 2], y[j + 2], z[j + 2]], dtype=x.dtype) - if coords_type == "spherical": - node1 = _lonlat_rad_to_xyz(np.deg2rad(x[0]), np.deg2rad(y[0])) - node1 = np.asarray(node1) - - node2 = _lonlat_rad_to_xyz(np.deg2rad(x[j + 1]), np.deg2rad(y[j + 1])) - node2 = np.asarray(node2) - - node3 = _lonlat_rad_to_xyz(np.deg2rad(x[j + 2]), np.deg2rad(y[j + 2])) - node3 = np.asarray(node3) - for p in range(len(dW)): if quadrature_rule == "gaussian": for q in range(len(dW)): @@ -92,9 +105,113 @@ def calculate_face_area( area += dW[p] * jacobian jacobian += jacobian + # check if the any edge is on the line of constant latitude + # which means we need to check edges for same z-coordinates and call area correction routine + correction = 0.0 + if correct_area: + for i in range(num_nodes): + node1 = np.array([x[i], y[i], z[i]], dtype=x.dtype) + node2 = np.array( + [ + x[(i + 1) % num_nodes], + y[(i + 1) % num_nodes], + z[(i + 1) % num_nodes], + ], + dtype=x.dtype, + ) + # Check if z-coordinates are approximately equal + if np.isclose(node1[2], node2[2]): + # Check if the edge passes through a pole + passes_through_pole = edge_passes_through_pole(node1, node2) + print("Check if edge passes through pole: ", passes_through_pole) + + if passes_through_pole: + # Skip the edge if it passes through a pole + continue + else: + # Calculate the correction term + correction = area_correction(node1, node2) + print("\n\ncalculated correction: ", correction) + # Check if the edge is in the northern or southern hemisphere + hemisphere = "" + if node1[2] > 0 and node2[2] > 0: + hemisphere = "Northern" + else: + hemisphere = "Southern" + if hemisphere != "": + print("Edge is in the ", hemisphere, " hemisphere.") + # Check if the edge goes from higher to lower longitude + # Convert Cartesian coordinates to longitude + lon1 = np.arctan2(node1[1], node1[0]) + lon2 = np.arctan2(node2[1], node2[0]) + + # Calculate the longitude difference in radians + lon_diff = lon2 - lon1 + + # Adjust for the case where longitude wraps around the 180° meridian + if lon_diff > np.pi: + lon_diff -= 2 * np.pi + elif lon_diff < -np.pi: + lon_diff += 2 * np.pi + + # Check if the longitude is increasing + if lon_diff > 0 and hemisphere == "Northern": + correction = -correction + elif lon_diff < 0 and hemisphere == "Southern": + correction = -correction + + print( + "For Node 1 ", + node1, + "\n and Node 2", + node2, + "\nCORRECTION", + correction, + ) + total_correction += correction + + if total_correction != 0.0: + print("AREA Before Correction", area) + + area += total_correction + + if total_correction != 0.0: + print("AREA After Correction", area) + return area, jacobian +@njit(cache=True) +def edge_passes_through_pole(node1, node2): + """ + Check if the edge passes through a pole. + + Parameters: + - node1: first node of the edge (normalized). + - node2: second node of the edge (normalized). + + Returns: + - bool: True if the edge passes through a pole, False otherwise. + """ + # Calculate the normal vector to the plane defined by the origin, node1, and node2 + n = np.cross(node1, node2) + + # Check for numerical stability issues with the normal vector + if np.allclose(n, 0): + # Handle cases where the cross product is near zero, such as when nodes are nearly identical or opposite + return False + + # Normalize the normal vector + n = n / np.linalg.norm(n) + + # North and South Pole vectors + p_north = np.array([0.0, 0.0, 1.0]) + p_south = np.array([0.0, 0.0, -1.0]) + + # Check if the normal vector is orthogonal to either pole + return np.isclose(np.dot(n, p_north), 0) or np.isclose(np.dot(n, p_south), 0) + + @njit(cache=True) def get_all_face_area_from_coords( x, @@ -106,6 +223,7 @@ def get_all_face_area_from_coords( quadrature_rule="triangular", order=4, coords_type="spherical", + correct_area=False, ): """Given coords, connectivity and other area calculation params, this routine loop over all faces and return an numpy array with areas of each @@ -141,6 +259,10 @@ def get_all_face_area_from_coords( ------- area of all faces : ndarray """ + # this casting helps to prevent the type mismatch + x = np.asarray(x, dtype=np.float64) + y = np.asarray(y, dtype=np.float64) + z = np.asarray(z, dtype=np.float64) n_face, n_max_face_nodes = face_nodes.shape @@ -161,7 +283,7 @@ def get_all_face_area_from_coords( # After getting all the nodes of a face assembled call the cal. face area routine face_area, face_jacobian = calculate_face_area( - face_x, face_y, face_z, quadrature_rule, order, coords_type + face_x, face_y, face_z, quadrature_rule, order, coords_type, correct_area ) # store current face area area[face_idx] = face_area @@ -170,6 +292,55 @@ def get_all_face_area_from_coords( return area, jacobian +@njit(cache=True) +def area_correction(node1, node2): + """ + Calculate the area correction A using the given formula. + + Parameters: + - node1: first node of the edge (normalized). + - node2: second node of the edge (normalized). + - z: z-coordinate (shared by both points and part of the formula, normalized). + + Returns: + - A: correction term of the area, when one of the edges is a line of constant latitude + """ + x1 = node1[0] + y1 = node1[1] + x2 = node2[0] + y2 = node2[1] + z = node1[2] + + # Calculate terms + term1 = x1 * y2 - x2 * y1 + den1 = x1**2 + y1**2 + x1 * x2 + y1 * y2 + den2 = x1 * x2 + y1 * y2 + + # Helper function to handle arctan quadrants + def arctan_quad(y, x): + if x > 0: + return np.arctan(y / x) + elif x < 0 and y >= 0: + return np.arctan(y / x) + np.pi + elif x < 0 and y < 0: + return np.arctan(y / x) - np.pi + elif x == 0 and y > 0: + return np.pi / 2 + elif x == 0 and y < 0: + return -np.pi / 2 + else: + return 0 # x == 0 and y == 0 case + + # Compute angles using arctan + angle1 = arctan_quad(z * term1, den1) + angle2 = arctan_quad(term1, den2) + + # Compute A + A = abs(2 * angle1 - z * angle2) + print(x1, y1, x2, y2, z, "correction:", A) + return A + + @njit(cache=True) def calculate_spherical_triangle_jacobian(node1, node2, node3, dA, dB): """Calculate Jacobian of a spherical triangle. This is a helper function diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 105841339..8312ff1d2 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1766,7 +1766,10 @@ def encode_as(self, grid_type: str) -> xr.Dataset: return out_ds def calculate_total_face_area( - self, quadrature_rule: Optional[str] = "triangular", order: Optional[int] = 4 + self, + quadrature_rule: Optional[str] = "triangular", + order: Optional[int] = 4, + correct_area: Optional[bool] = False, ) -> float: """Function to calculate the total surface area of all the faces in a mesh. @@ -1784,7 +1787,9 @@ def calculate_total_face_area( """ # call function to get area of all the faces as a np array - face_areas, face_jacobian = self.compute_face_areas(quadrature_rule, order) + face_areas, face_jacobian = self.compute_face_areas( + quadrature_rule, order, correct_area=correct_area + ) return np.sum(face_areas) @@ -1793,6 +1798,7 @@ def compute_face_areas( quadrature_rule: Optional[str] = "triangular", order: Optional[int] = 4, latlon: Optional[bool] = True, + correct_area: Optional[bool] = False, ): """Face areas calculation function for grid class, calculates area of all faces in the grid. @@ -1860,6 +1866,7 @@ def compute_face_areas( quadrature_rule, order, coords_type, + correct_area, ) min_jacobian = np.min(self._face_jacobian)