diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7657dfdcb..52e5160c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,13 +18,14 @@ import uxarray as ux grid_path = "/path/to/grid.nc" data_path = "/path/to/data.nc" -uxgrid = ux.Grid(grid_path) +uxds = ux.open_dataset(grid_path, data_path) # this is how you use this function -some_output = uxgrid.some_function() +some_output = uxds.some_function() # this is another way to use this function -other_output = uxgrid.some_function(some_param = True) +other_output = uxds.some_function(some_param = True) + ``` ## PR Checklist diff --git a/README.md b/README.md index 0775c6310..0f617061a 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ UXarray aims to address the geoscience community need for tools that enable standard data analysis techniques to operate directly on unstructured grid -data. UXarray will provide Xarray styled functions to better read in and use +data. UXarray provides Xarray styled functions to better read in and use unstructured grid datasets that follow standard conventions, including UGRID, -SCRIP, Exodus and shapefile formats. This effort is a result of the +MPAS, SCRIP, and Exodus formats. This effort is a result of the collaboration between Project Raijin (NCAR and Pennsylvania State University) and the SEATS Project (Argonne National Laboratory, UC Davis, and Lawrence Livermore National Laboratory). The UXarray team welcomes other community @@ -26,11 +26,11 @@ under the `Issues` tab in the UXarray repository. # Why is the name "UXarray"? We have created UXarray based on [Xarray](https://docs.xarray.dev/en/stable/) -(via composition of a Xarray dataset object), a Pangeo ecosystem package +(via inheritance of Xarray Dataset and DataArray classes), a Pangeo ecosystem package commonly-used for structured grids recognition, to support reading and recognizing unstructured grid model outputs. We picked the name "UXarray" -and preferred to capitalize the first two letters to emphasize it is Xarray -for Unstructured grids. +(pronounced "you-ex-array") and preferred to capitalize the first two letters to +emphasize it builds upon Xarray for Unstructured grids. # UXarray Functionality diff --git a/docs/announcement.rst b/docs/announcement.rst new file mode 100644 index 000000000..5135dfde6 --- /dev/null +++ b/docs/announcement.rst @@ -0,0 +1,23 @@ +.. currentmodule:: uxarray + +.. _announcement: + +Announcement +============ + +v2023.6.0 of UXarray includes significant code design & API changes +to address the user needs and requests better. Further information +about the design change can be found in several places such as: + +- v2023.6.0 Release notes + +- `GitHub Discussion - UXarray Redesign Thoughts and + Options `_ + +- `GitHub Pull Request #216 - + Redesign `_ that implemented the new design with all the code and documentation changes + +- `Usage Examples `_ can be utilized to get to know the new design + + +Feel free to reach out to us with any questions/concerns. diff --git a/docs/api.rst b/docs/api.rst index 8910ca3cc..bc14ff970 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,8 +5,10 @@ API Reference ============= -Below user API and internal API pages show already-implemented Uxarray functionality. -You can also check the draft `Uxarray API + + +Below user API and internal API pages show already-implemented UXarray functionality. +You can also check the draft `UXarray API `_ documentation to see the tentative whole API and let us know if you have any feedback! diff --git a/docs/citation.rst b/docs/citation.rst index f7827195b..176616734 100644 --- a/docs/citation.rst +++ b/docs/citation.rst @@ -2,17 +2,17 @@ .. _citation: -How to Cite Uxarray +How to Cite UXarray =================== -Cite Uxarray using the following text: +Cite UXarray using the following text: -**UXARRAY Organization. (Year). -Uxarray (version \) [Software]. +**UXarray Organization. (Year). +UXarray (version \) [Software]. Project Raijin & Project SEATS. doi:10.5281/zenodo..** -Update the year, Uxarray version, and DOI part for UXarray version as appropriate. For example: +Update the year, UXarray version, and DOI part for UXarray version as appropriate. For example: -**UXARRAY Organization. (2021). -Uxarray (version 0.0.2) [Software]. +**UXarray Organization. (2021). +UXarray (version 0.0.2) [Software]. Project Raijin & Project SEATS. doi:10.5281/zenodo.5658256.** diff --git a/docs/examples.rst b/docs/examples.rst index 312eb5eef..fb1021fd3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -5,9 +5,8 @@ Usage Examples ============== -Here's some examples of how to use the uxarray. We are always planning to add -more examples! If you are interested in contributing your own examples, please -see the :doc:`contributing`. +Below is a list of usage examples showcasing how to use UXarray. If you are interested in contributing +your own examples or suggesting one for us to create, please see the :doc:`contributing`. .. include:: notebook-examples.txt @@ -15,6 +14,6 @@ see the :doc:`contributing`. :maxdepth: 1 :hidden: - examples/001-read-grid-data.ipynb - examples/002-access-grid-info.ipynb + examples/001-working-with-unstructured-grids.ipynb + examples/002-grid-topology.ipynb examples/003-area-calc.ipynb diff --git a/docs/examples/001-read-grid-data.ipynb b/docs/examples/001-read-grid-data.ipynb deleted file mode 100644 index 3fa71b8bf..000000000 --- a/docs/examples/001-read-grid-data.ipynb +++ /dev/null @@ -1,240 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Reading In Grid Data\n", - "\n", - "UXarray offers support for loading and representing unstructured grids\n", - "by utilizing existing Xarray functionality paired with new routines that\n", - "are specifically written for operating on unstructured grids.\n" - ] - }, - { - "cell_type": "markdown", - "source": [ - "## Overview\n", - "\n", - "When working with unstructured grids, the grid definition and data variables\n", - "are often stored as separate files (most of the time as netCDF). This means\n", - "that there are multiple separate files that need to be read in at once.\n", - "\n", - "For example, the NOAA Geoflow project consists of 4 files (1 grid file and 3\n", - "data files). This project follows the UGRID conventions. Special thanks to\n", - "John Clyne, Shilpi Gupta, and the VAPOR team for providing these data!\n", - "\n", - "```\n", - "geoflow-small\n", - "│ grid.nc\n", - "│ v1.nc\n", - "│ v2.nc\n", - "│ v3.nc\n", - "```" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## Opening The Grid and Data Files with Xarray\n", - "\n", - "First, read in the grid definition and data variable netCCDF files by using\n", - "`xarray.open_dataset`. In addition, `xr.open_mf_dataset` can be used for\n", - "combining multiple data variables into a single `xarray.Dataset`." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import xarray as xr" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# Base data path\n", - "base_path = \"../../test/meshfiles/ugrid/geoflow-small/\"\n", - "\n", - "# Path to Grid file\n", - "grid_path = base_path + \"grid.nc\"\n", - "\n", - "# Paths to Data Variable files\n", - "var_names = ['v1.nc', 'v2.nc', 'v3.nc']\n", - "data_paths = [base_path + name for name in var_names]" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\Philip\\projects\\uxarray-redesign\\venv\\lib\\site-packages\\xarray\\backends\\plugins.py:71: RuntimeWarning: Engine 'cfgrib' loading failed:\n", - "Cannot find the ecCodes library\n", - " warnings.warn(f\"Engine {name!r} loading failed:\\n{ex}\", RuntimeWarning)\n" - ] - } - ], - "source": [ - "# Open the Grid file\n", - "grid_ds = xr.open_dataset(grid_path)\n", - "\n", - "# Open a single file or multiple files\n", - "v1_ds = xr.open_dataset(data_paths[0])\n", - "multi_data_ds = xr.open_mfdataset(data_paths)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "outputs": [ - { - "data": { - "text/plain": "\nDimensions: (nMeshFaces: 3840, nFaceNodes: 4, nMeshNodes: 6000,\n meshLayers: 20)\nCoordinates:\n mesh_node_x (nMeshNodes) float64 ...\n mesh_node_y (nMeshNodes) float64 ...\nDimensions without coordinates: nMeshFaces, nFaceNodes, nMeshNodes, meshLayers\nData variables:\n mesh int32 ...\n mesh_face_nodes (nMeshFaces, nFaceNodes) float64 ...\n mesh_depth (meshLayers, nMeshNodes) float64 ...", - "text/html": "
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
<xarray.Dataset>\nDimensions:          (nMeshFaces: 3840, nFaceNodes: 4, nMeshNodes: 6000,\n                      meshLayers: 20)\nCoordinates:\n    mesh_node_x      (nMeshNodes) float64 ...\n    mesh_node_y      (nMeshNodes) float64 ...\nDimensions without coordinates: nMeshFaces, nFaceNodes, nMeshNodes, meshLayers\nData variables:\n    mesh             int32 ...\n    mesh_face_nodes  (nMeshFaces, nFaceNodes) float64 ...\n    mesh_depth       (meshLayers, nMeshNodes) float64 ...
" - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid_ds" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 8, - "outputs": [ - { - "data": { - "text/plain": "\nDimensions: (time: 1, meshLayers: 20, nMeshNodes: 6000)\nCoordinates:\n * time (time) float64 13.0\nDimensions without coordinates: meshLayers, nMeshNodes\nData variables:\n v1 (time, meshLayers, nMeshNodes) float64 dask.array\n v2 (time, meshLayers, nMeshNodes) float64 dask.array\n v3 (time, meshLayers, nMeshNodes) float64 dask.array", - "text/html": "
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
<xarray.Dataset>\nDimensions:  (time: 1, meshLayers: 20, nMeshNodes: 6000)\nCoordinates:\n  * time     (time) float64 13.0\nDimensions without coordinates: meshLayers, nMeshNodes\nData variables:\n    v1       (time, meshLayers, nMeshNodes) float64 dask.array<chunksize=(1, 20, 6000), meta=np.ndarray>\n    v2       (time, meshLayers, nMeshNodes) float64 dask.array<chunksize=(1, 20, 6000), meta=np.ndarray>\n    v3       (time, meshLayers, nMeshNodes) float64 dask.array<chunksize=(1, 20, 6000), meta=np.ndarray>
" - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "multi_data_ds" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## Representing The Unstructured Grid with UXarray" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 9, - "outputs": [], - "source": [ - "import uxarray as ux" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "code", - "execution_count": 10, - "outputs": [], - "source": [ - "# Construct a UXarray Grid object from `grid_ds`\n", - "grid = ux.Grid(grid_ds)" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 11, - "outputs": [ - { - "data": { - "text/plain": "\nDimensions: (nMeshFaces: 3840, nFaceNodes: 4, nMeshNodes: 6000,\n meshLayers: 20)\nCoordinates:\n mesh_node_x (nMeshNodes) float64 ...\n mesh_node_y (nMeshNodes) float64 ...\nDimensions without coordinates: nMeshFaces, nFaceNodes, nMeshNodes, meshLayers\nData variables:\n mesh int32 ...\n mesh_face_nodes (nMeshFaces, nFaceNodes) float64 ...\n mesh_depth (meshLayers, nMeshNodes) float64 ...", - "text/html": "
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
<xarray.Dataset>\nDimensions:          (nMeshFaces: 3840, nFaceNodes: 4, nMeshNodes: 6000,\n                      meshLayers: 20)\nCoordinates:\n    mesh_node_x      (nMeshNodes) float64 ...\n    mesh_node_y      (nMeshNodes) float64 ...\nDimensions without coordinates: nMeshFaces, nFaceNodes, nMeshNodes, meshLayers\nData variables:\n    mesh             int32 ...\n    mesh_face_nodes  (nMeshFaces, nFaceNodes) float64 ...\n    mesh_depth       (meshLayers, nMeshNodes) float64 ...
" - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid.ds" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "As can be seen, the Grid object has its own `grid.ds` of type `xarray.Dataset`\n", - "to define the grid topology. However, the Grid object has further attributes,\n", - "variables, and functions to specify the unstructured grid and be executed on\n", - "it, which can be explored in the next notebooks." - ], - "metadata": { - "collapsed": false - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.10.5 ('uxarray_build')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.5" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "8e8ae2f388051fced6c30f82a529eeca8cf1e059ab06a64326e2a2ad0ec3c36c" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/examples/001-working-with-unstructured-grids.ipynb b/docs/examples/001-working-with-unstructured-grids.ipynb new file mode 100644 index 000000000..8fad0b4c8 --- /dev/null +++ b/docs/examples/001-working-with-unstructured-grids.ipynb @@ -0,0 +1,391 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with Unstructured Grid Data\n", + "\n", + "UXarray offers support for loading and representing unstructured grids\n", + "by providing Xarray-like functionality paired with new routines that\n", + "are specifically written for operating on unstructured grids.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Definition and Data Variables\n", + "\n", + "When working with Unstructured Grids, the grid definition and data variables\n", + "are often stored as separate files. This means that there are multiple\n", + "separate files that need to be read and linked together to represent the\n", + "entire dataset.\n", + "\n", + "For example, the following sample dataset is taken from the NOAA Geoflow project,\n", + "which is made up of 4 files: 1 grid definition and 3 data files. (Special thanks to John Clyne, Shilpi Gupta, and the VAPOR team for providing this data!)\n", + "\n", + "```\n", + "geoflow-small\n", + "│ grid.nc\n", + "│ v1.nc\n", + "│ v2.nc\n", + "│ v3.nc\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Conventions\n", + "\n", + "Given the complexity of Unstructured Grids, there are many different ways of representing their underlying topology and structure. These representations are referred to as conventions, and they outline\n", + "the required connectivity variables, naming conventions, data types, and many other specifications. UXarray uses the [UGRID](http://ugrid-conventions.github.io/ugrid-conventions/)\n", + "conventions as a foundation for internally representing Unstructured Grids, converting any supported input grid format into the UGRID convention at the data loading step. Below is a list of supported formats and conventions that can be read in with UXarray:\n", + "* UGRID\n", + "* Model for Prediction Across Scales (MPAS)\n", + "* Exodus\n", + "\n", + "In addition to loading datasets, we also provide support for constructing a grid from user-defined primitives such as vertices, which is showcased in our other notebooks.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reading Grid and Data Files\n", + "UXarray provides the `UxDataset` data structure, which is an unstructure grid-informed implementation of Xarray's `Dataset` class. The main addition is the introduction of the `uxgrid` property, which stores our grid topology dimensions, coordinates, variables and provides grid-specific functions.\n", + "\n", + "Constructing a `UxDataset` can be done using our custom `open_dataset` and `open_mfdataset` methods, depending on whether one or multiple data files or objects are meant to be linked to a single grid.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import uxarray as ux\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Base data path\n", + "base_path = \"../../test/meshfiles/ugrid/geoflow-small/\"\n", + "\n", + "# Path to Grid file\n", + "grid_path = base_path + \"grid.nc\"\n", + "\n", + "# Paths to Data Variable files\n", + "var_names = ['v1.nc', 'v2.nc', 'v3.nc']\n", + "\n", + "data_paths = [base_path + name for name in var_names]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Loading a single data file with a grid is done using the `open_dataset` method. The resulting `UxDataset` only contains the data variables stored in `v1.nc`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_single = ux.open_dataset(grid_path, data_paths[0])\n", + "uxds_single" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, if you wish to open multiple data files with a grid, you would use the `open_mfdataset` method. The resulting `UxDataset` contains all the data variables stored in `v1.nc`, `v2.nc`, and `v3.nc`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_multiple = ux.open_mfdataset(grid_path, data_paths)\n", + "uxds_multiple" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Grid Topology" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each dataset contains the aforementioned `uxgrid` property, which is a `Grid` object and represents the grid topology that the data variables lie on. The `uxgrid` property can be used to execute grid specific functions and access grid topology dimensions, coordinates, and variables. A detailed overview of functionalities can be found in subsequent notebooks.\n", + "\n", + "For both instances of `UxDataset` that contain single and multiple data sets (i.e. `uxds_single` and `uxds_multiple`), the `uxgrid` property contains the same grid information, however they are each instantiated separately.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# check if the grids contain the same variables & information\n", + "print(uxds_single.uxgrid == uxds_multiple.uxgrid)\n", + "\n", + "# check if the grids point to the same object in memory\n", + "print(uxds_single.uxgrid is uxds_multiple.uxgrid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Printing out the `uxgrid` property provides an overview of the original grid format, dimensions, coordinates, and connectivity variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_multiple.uxgrid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These dimensions, coordinates, and connectivity variables can be accessed with attributes using the same names as shown in the print-out. Below are a few examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_multiple.uxgrid.nMesh2_node" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_multiple.uxgrid.Mesh2_node_x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_multiple.uxgrid.Mesh2_face_nodes" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Data Variables" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "While grid-specific variables and functions are stored under the `uxgrid` property, data variables that lie on the grid are stored directly in the `UxDataset` or `UxDataArray`. Most `Xarray` functions and operators can be executed on these data structures.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single.values" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single.dims" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single.coords" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single.attrs" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single.min()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_single > 0" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "grid = uxds_single.uxgrid\n", + "foo = ux.UxDataArray(\n", + " data = np.random.random(grid.nMesh2_face),\n", + " dims = [\"nMesh2_face\"],\n", + " uxgrid = grid\n", + ")\n", + "foo" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_new_var = uxds_single.assign({\"foo\" : foo})" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "uxds_new_var" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + }, + "vscode": { + "interpreter": { + "hash": "8e8ae2f388051fced6c30f82a529eeca8cf1e059ab06a64326e2a2ad0ec3c36c" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/examples/002-access-grid-info.ipynb b/docs/examples/002-access-grid-info.ipynb deleted file mode 100644 index 3e51c5b47..000000000 --- a/docs/examples/002-access-grid-info.ipynb +++ /dev/null @@ -1,287 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "# Accessing Grid Information\n", - "\n", - "Unstructured grids can be represented in one of many different conventions\n", - "(UGRID, SCRIP, EXODUS, etc). These conventions have different definitions\n", - "and representations of the attributes and variables used to describe\n", - "the unstructured grid topology. Even more, the [UGRID conventions](\n", - "https://ugrid-conventions.github.io/ugrid-conventions/) does not\n", - "enforce standard variable namings for most of the attributes and variables\n", - "(other than just a few required ones).\n", - "\n", - "UXarray unifies all of these conventions at the data loading step by\n", - "representing grids in the UGRID convention regardless of the original grid\n", - "type that is read in from the file. Furthermore, it uses a set of\n", - "standardized names for topology attributes and variables, while still\n", - "providing the user with the original attribute names and variables that\n", - "came from the grid definition file.\n", - "\n", - "## Overview\n", - "\n", - "This notebook will showcase the different methods available for accessing\n", - "the grid topology attributes and variables stored in the `UXarray.Grid`\n", - "object.\n", - "\n", - "For more details on how to load in data, check out our [previous usage\n", - "example](https://uxarray.readthedocs.io/en/latest/examples/read-grid-data.html)\n", - "\n", - "**Methods**\n", - "1. Indexing with Original Variable Names\n", - "2. Indexing with UXarray Variable Dictionary\n", - "3. UXarray's Standardized UGRID Names (Most convenient)" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## Data\n", - "\n", - "We will be using two grid files, both of which are in the UGRID convention.\n", - "However, the key difference between them is the names used to describe the\n", - "attributes and variables.\n", - "\n", - "Let us first read in the data:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 4, - "outputs": [], - "source": [ - "import uxarray as ux\n", - "import xarray as xr" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 5, - "outputs": [], - "source": [ - "# Base data path\n", - "base_path = \"../../test/meshfiles/\"\n", - "\n", - "# Path to Grid files\n", - "ugrid_01_path = base_path + \"/ugrid/outCSne30/outCSne30.ug\"\n", - "ugrid_02_path = base_path + \"/ugrid/geoflow-small/grid.nc\"\n", - "\n", - "# Load grid files and create UXarray Grid objects\n", - "ugrid_01_ds = xr.open_dataset(ugrid_01_path)\n", - "ugrid_02_ds = xr.open_dataset(ugrid_02_path)\n", - "\n", - "ugrid_01 = ux.Grid(ugrid_01_ds)\n", - "ugrid_02 = ux.Grid(ugrid_02_ds)" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "The output of the bottom cell showcases the slight differences\n", - "in variable names:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 16, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Variable Names\n", - "ugrid_01 variable names: ['Mesh2', 'Mesh2_face_nodes', 'Mesh2_node_x', 'Mesh2_node_y', 'nMesh2_face', 'nMaxMesh2_face_nodes', 'nMesh2_node']\n", - "ugrid_02 variable names: ['mesh', 'mesh_face_nodes', 'mesh_depth', 'mesh_node_x', 'mesh_node_y', 'nMeshFaces', 'nFaceNodes', 'nMeshNodes', 'meshLayers']\n" - ] - } - ], - "source": [ - "# Extract variable names\n", - "ugrid_01_names = list(ugrid_01.ds.keys()) + \\\n", - " list(ugrid_01.ds.coords) + \\\n", - " list(ugrid_01.ds.dims)\n", - "ugrid_02_names = list(ugrid_02.ds.keys()) + \\\n", - " list(ugrid_02.ds.coords) + \\\n", - " list(ugrid_02.ds.dims)\n", - "\n", - "print(\"\\nAttribute and variable names for each grid:\")\n", - "print(\"ugrid_01 variable names:\", ugrid_01_names)\n", - "print(\"ugrid_02 variable names:\", ugrid_02_names)" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## 1. Indexing with Original Variable Names\n", - "\n", - "The simplest approach is to use the original variable name as an index\n", - "into the grid dataset, `Grid.ds`. Since `ugrid_01` and `ugrid_02` have\n", - "different names for most of their topology attributes and variables, the\n", - "index for accessing them will be different for both grids." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 20, - "outputs": [], - "source": [ - "x_1 = ugrid_01.ds['Mesh2_node_x']\n", - "y_1 = ugrid_01.ds['Mesh2_node_y']\n", - "face_nodes_1 = ugrid_01.ds['Mesh2_face_nodes']\n", - "n_face_nodes_1 = ugrid_01.ds['nMaxMesh2_face_nodes']\n", - "\n", - "x_2 = ugrid_02.ds['mesh_node_x']\n", - "y_2 = ugrid_02.ds['mesh_node_y']\n", - "face_nodes_2 = ugrid_02.ds['mesh_face_nodes']\n", - "n_face_nodes_2 = ugrid_02.ds['nFaceNodes']" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## 2. Indexing with UXarray Variable Dictionary\n", - "\n", - "UXarray provides a dictionary, `Grid.ds_var_names`, to map the original\n", - "topology attribute and variable names that come from the grid file into\n", - "a standardized set of names. In other words, the dictionary uses a\n", - "standardized set of UGRID attribute and variable names as keys, and the\n", - "original variable names that come from the grid file as values.\n", - "\n", - "This allows us to use the same index into either dataset. However, this\n", - "makes the indexing code much longer than the previous method." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 24, - "outputs": [], - "source": [ - "var_names_dict = ugrid_01.ds_var_names\n", - "x_1 = ugrid_01.ds[var_names_dict['Mesh2_node_x']]\n", - "y_1 = ugrid_01.ds[var_names_dict['Mesh2_node_y']]\n", - "face_nodes_1 = ugrid_01.ds[var_names_dict['Mesh2_face_nodes']]\n", - "n_face_nodes_1 = ugrid_01.ds[var_names_dict['nMesh2_node']]\n", - "\n", - "var_names_dict = ugrid_02.ds_var_names\n", - "x_2 = ugrid_02.ds[var_names_dict['Mesh2_node_x']]\n", - "y_2 = ugrid_02.ds[var_names_dict['Mesh2_node_y']]\n", - "face_nodes_2 = ugrid_02.ds[var_names_dict['Mesh2_face_nodes']]\n", - "n_face_nodes_2 = ugrid_02.ds[var_names_dict['nMesh2_node']]" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "Please note, for instance, we accessed the actual variable `mesh_node_x`\n", - "of `ugrid_02` via indexing the dictionary with the standardized name\n", - "`Mesh2_node_x`, likewise in `ugrid_01`." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "## 3. UXarray's Standardized UGRID Names\n", - "The last way of accessing grid topology attributes and variables is to use\n", - "the standardized UGRID namings provided by UXarray. This method still\n", - "utilizes the dictionary, `ds_var_names`, under the hood to return a\n", - "reference to the variable or attribute that is stored withing\n", - "`UXarray.Grid.ds`.\n", - "\n", - "This eliminates the need to remember the original variable names and\n", - "needing to index through the `ds_var_names` dictionary. Because of this,\n", - "we find this as the most convenient approach." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 22, - "outputs": [], - "source": [ - "x_1 = ugrid_01.Mesh2_node_x\n", - "y_1 = ugrid_01.Mesh2_node_y\n", - "face_nodes_1 = ugrid_01.Mesh2_face_nodes\n", - "n_face_nodes_1 = ugrid_01.nMesh2_node\n", - "\n", - "x_2 = ugrid_02.Mesh2_node_x\n", - "y_2 = ugrid_02.Mesh2_node_y\n", - "face_nodes_2 = ugrid_02.Mesh2_face_nodes\n", - "n_face_nodes_2 = ugrid_02.nMesh2_node" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "In conclusion, there are three ways of accessing the grid attributes and\n", - "variables. Even though the UXarray developers recommend using the\n", - "standardized UGRID names method, users can find each various pros/cons with\n", - "each of these access ways." - ], - "metadata": { - "collapsed": false - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/docs/examples/002-grid-topology.ipynb b/docs/examples/002-grid-topology.ipynb new file mode 100644 index 000000000..f2a8274e8 --- /dev/null +++ b/docs/examples/002-grid-topology.ipynb @@ -0,0 +1,555 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Grid Topology Overview\n", + "\n", + "As discussed in our first notebook, UXarray uses the UGRID conventions as a foundation for represented Unstructured Grids. Here we'll see how to access the underlying dimensions, coordinates, and connectivity variables that are available in UXarray." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Grid Topology" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The grid topology is defined as a `Grid` object in UXarray. A `Grid` object can be created in different ways. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constructing through `uxarray.open_grid()`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the intent is to explore only the grid topology instead of having any data sets with it, analyzing data variables, etc., a standalone `Grid` object can be instantiated as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import uxarray\n", + "import uxarray as ux" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Base data path\n", + "base_path = \"../../test/meshfiles/\"\n", + "\n", + "# Grid Path (MPAS Example)\n", + "grid_mpas_path = base_path + \"/mpas/QU/mesh.QU.1920km.151026.nc\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid_mpas = ux.open_grid(grid_mpas_path)\n", + "grid_mpas" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constructing through `UxDataset`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the intent is to have an unstructured grid-aware dataset, i.e. `UxDataset`, and investigate the grid topology through it instead, the `uxgrid` property can be used. To be more precise, when a `UxDataset` object, or likewise `UxDataArray` object, is generated (e.g. through `uxarray.open_dataset()`), a `Grid` object via `UxDataset.uxgrid`, or `UxDataArray.uxgrid`, property is also created automatically and assigned to the dataset or variable. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T05:51:09.904564Z", + "start_time": "2023-06-09T05:51:08.080955Z" + }, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "The grid topology can be seen through this property as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Data File Path (UGRID Example)\n", + "grid_ne30_path = base_path + \"/ugrid/outCSne30/outCSne30.ug\"\n", + "data_ne30_path = base_path + \"/ugrid/outCSne30/outCSne30_vortex.nc\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds = ux.open_dataset(grid_ne30_path, data_ne30_path)\n", + "uxds.uxgrid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition, a previously-created `Grid` object can always be assigned to a new `UxDataset` as its `uxgrid` property as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_mpas = ux.UxDataset(uxgrid=grid_mpas)\n", + "uxds_mpas.uxgrid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Attributes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All of the grid topology attributes such as coordinates, connectivity variables, dimensions, etc can be examined through the `uxgrid` property, i.e. `Grid` object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If our input grid contained additional attributes that were not representable by the UGRID conventions, they would be stored here" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.parsed_attrs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds_mpas.uxgrid.parsed_attrs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Coordinates" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The coordinates by default are represented in terms of longitude and latitude." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_node_x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_node_y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you wish to use the Cartesian coordinate system, you can access the following attributes, which will internally construct a set of Cartesian coordinates derived from the previous set.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_node_cart_x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_node_cart_y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_node_cart_z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Connectivity\n", + "\n", + "Connectivity variables are used to describe how various geometric elements (nodes, faces, edges) can be manipulated and interconnected to represent the topology of the unstructured grid." + ] + }, + { + "cell_type": "markdown", + "source": [ + "As described in the UGRID conventions, these connectivity variables are stored as integer arrays and may contain a Fill Value. UXarray standardizes both of these at the data loading step, meaning that the data type and fill value can always be guaranteed to be the following:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "ux.INT_DTYPE" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "ux.INT_FILL_VALUE" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "Below we can see how to access these connectivity variables." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_face_nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nNodes_per_face" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see above, these are the only two connectivity variables listed. In addition to these, UXarray provides support for constructing additional connectivity variables.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_edge_nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.Mesh2_face_edges" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These additional variables are constructed upon calling their respective attributes and are now stored under the `uxgrid` property. Additionally, the `Mesh2_node_cart_x`, `Mesh2_node_cart_y`, and `Mesh2_node_cart_z` that we constructed earlier are now also shown here.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Grid Dimensions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nMesh2_node" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nMesh2_edge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nMesh2_face" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nMaxMesh2_face_nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "uxds.uxgrid.nMaxMesh2_face_edges" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/examples/003-area-calc.ipynb b/docs/examples/003-area-calc.ipynb index 096fabaeb..c60cebc8d 100644 --- a/docs/examples/003-area-calc.ipynb +++ b/docs/examples/003-area-calc.ipynb @@ -1,11 +1,8 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "# Face Area Calculations\n", "\n", @@ -25,24 +22,24 @@ ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "We will be using the `outCSne30.ug` grid file, which is encoded in the UGRID convention." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:02.171354Z", - "start_time": "2023-04-19T09:57:02.144399Z" + "end_time": "2023-06-09T10:04:28.775400Z", + "start_time": "2023-06-09T10:04:28.705400Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -53,15 +50,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:02.172124Z", - "start_time": "2023-04-19T09:57:02.148439Z" + "end_time": "2023-06-09T10:04:28.817401Z", + "start_time": "2023-06-09T10:04:28.718401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Original Grid Type: ugrid\n", + "Grid Dimensions:\n", + " * nMesh2_face: 5400\n", + " * nMaxMesh2_face_nodes: 4\n", + " * nMesh2_node: 5402\n", + "Grid Coordinate Variables:\n", + " * Mesh2_node_x: (5402,)\n", + " * Mesh2_node_y: (5402,)\n", + "Grid Connectivity Variables:\n", + " * Mesh2_face_nodes: (5400, 4)\n", + " * nNodes_per_face: (5400,)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Base data path\n", "base_path = \"../../test/meshfiles/\"\n", @@ -73,42 +95,50 @@ "ugrid_ds = xr.open_dataset(ugrid_path)\n", "\n", "ugrid = ux.Grid(ugrid_ds)\n", - "ugrid.ds" + "ugrid" ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "## 1. Calculate Total Face Area\n", - "We can calculate the total face area by calling the function `Grid.calculate_total_face_area()`. Since our dataset lies on the unit sphere, our expected area is 4pi, which is approximatly 12.56" + "We can calculate the total face area by calling the function `Grid.calculate_total_face_area()`. Since our dataset lies on the unit sphere, our expected area is 4*pi, which is approximately 12.56" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.685968Z", - "start_time": "2023-04-19T09:57:02.154548Z" + "end_time": "2023-06-09T10:04:28.820400Z", + "start_time": "2023-06-09T10:04:28.732401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "12.566370614678554" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "t4_area = ugrid.calculate_total_face_area()\n", "t4_area" ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "## 2. Options for `Grid.calculate_total_face_area` Function\n", "\n", @@ -124,21 +154,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.690062Z", - "start_time": "2023-04-19T09:57:03.687612Z" + "end_time": "2023-06-09T10:04:28.821401Z", + "start_time": "2023-06-09T10:04:28.777401Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "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" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -152,11 +192,8 @@ ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "## 3. Getting Area of Individual Faces\n", "\n", @@ -166,39 +203,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.693036Z", - "start_time": "2023-04-19T09:57:03.690838Z" + "end_time": "2023-06-09T10:04:28.827402Z", + "start_time": "2023-06-09T10:04:28.810401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.00211238, 0.00211285, 0.00210788, ..., 0.00210788, 0.00211285,\n", + " 0.00211238])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "ugrid.face_areas" ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "Now calculate the area again with the `Grid.compute_face_areas` function using arguments: quadrature_rule \"gaussian\" and order 4" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.696254Z", - "start_time": "2023-04-19T09:57:03.693676Z" + "end_time": "2023-06-09T10:04:28.904401Z", + "start_time": "2023-06-09T10:04:28.896401Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "12.566370614359112" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "all_face_areas = ugrid.compute_face_areas(quadrature_rule=\"gaussian\", order=4)\n", "g4_area = all_face_areas.sum()\n", @@ -206,7 +266,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -215,9 +274,25 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T10:04:28.950400Z", + "start_time": "2023-06-09T10:04:28.904401Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.005033379360810386, 3.1938185429680743e-10, 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", @@ -229,19 +304,14 @@ }, { "cell_type": "markdown", + "metadata": {}, "source": [ "As we can see, it is clear that the Gaussian Quadrature Rule with Order 4 is the most accurate, and the Triangular Quadrature Rule with Order 1 is the least accurate.\n" - ], - "metadata": { - "collapsed": false - } + ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "## 4. Calculate Area of a Single Triangle in Cartesian Coordinates\n", "\n", @@ -252,15 +322,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.703400Z", - "start_time": "2023-04-19T09:57:03.696048Z" + "end_time": "2023-06-09T10:04:28.958400Z", + "start_time": "2023-06-09T10:04:28.921401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Original Grid Type: From vertices\n", + "Grid Dimensions:\n", + " * nMesh2_node: 3\n", + " * nMesh2_face: 1\n", + " * nMaxMesh2_face_nodes: 3\n", + "Grid Coordinate Variables:\n", + " * Mesh2_node_x: (3,)\n", + " * Mesh2_node_y: (3,)\n", + "Grid Connectivity Variables:\n", + " * Mesh2_face_nodes: (1, 3)\n", + " * nNodes_per_face: (1,)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "verts = [[[0.57735027, -5.77350269e-01, -0.57735027],\n", " [0.57735027, 5.77350269e-01, -0.57735027],\n", @@ -272,26 +367,39 @@ " islatlon=False,\n", " concave=False)\n", "\n", - "vgrid.ds" + "vgrid" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.705404Z", - "start_time": "2023-04-19T09:57:03.703896Z" + "end_time": "2023-06-09T10:04:28.967401Z", + "start_time": "2023-06-09T10:04:28.951402Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "1.0719419938548207" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "vgrid.calculate_total_face_area()" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -300,37 +408,49 @@ }, { "cell_type": "code", - "execution_count": null, - "outputs": [], + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T10:04:29.014400Z", + "start_time": "2023-06-09T10:04:28.967401Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0475702709086985" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Set correct units for the x and y coordinates\n", "vgrid.Mesh2_node_x.attrs[\"units\"] = \"km\"\n", "vgrid.Mesh2_node_y.attrs[\"units\"] = \"km\"\n", - "vgrid.Mesh2_node_z.attrs[\"units\"] = \"km\"\n", + "vgrid._Mesh2_node_z.attrs[\"units\"] = \"km\" # This is just a placeholder, UXarray does not support 3D meshes \n", "\n", "# Calculate the area of the triangle\n", "area_gaussian = vgrid.calculate_total_face_area(\n", " quadrature_rule=\"gaussian\", order=5)\n", "area_gaussian" - ], - "metadata": { - "collapsed": false - } + ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "metadata": {}, "source": [ "## 5. Calculate Area from Multiple Faces in Spherical Coordinates\n", "\n", @@ -339,13 +459,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.733266Z", - "start_time": "2023-04-19T09:57:03.712481Z" + "end_time": "2023-06-09T10:04:29.022400Z", + "start_time": "2023-06-09T10:04:28.984401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -361,49 +484,85 @@ }, { "cell_type": "markdown", + "metadata": {}, "source": [ "We want our units to be spherical, so we pass through `islatlon=True`. Additionally, if `islatlon` is not passed through, it will default to spherical coordinates." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.733502Z", - "start_time": "2023-04-19T09:57:03.715386Z" + "end_time": "2023-06-09T10:04:29.046402Z", + "start_time": "2023-06-09T10:04:29.001401Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Original Grid Type: From vertices\n", + "Grid Dimensions:\n", + " * nMesh2_node: 14\n", + " * nMesh2_face: 3\n", + " * nMaxMesh2_face_nodes: 6\n", + "Grid Coordinate Variables:\n", + " * Mesh2_node_x: (14,)\n", + " * Mesh2_node_y: (14,)\n", + "Grid Connectivity Variables:\n", + " * Mesh2_face_nodes: (3, 6)\n", + " * nNodes_per_face: (3,)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "verts_grid = ux.Grid(faces_verts_ndarray,\n", " vertices=True,\n", " islatlon=True,\n", " concave=False)\n", - "verts_grid.ds" + "verts_grid" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "ExecuteTime": { - "end_time": "2023-04-19T09:57:03.733582Z", - "start_time": "2023-04-19T09:57:03.724024Z" + "end_time": "2023-06-09T10:04:29.078400Z", + "start_time": "2023-06-09T10:04:29.034401Z" }, - "collapsed": false + "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": [ "verts_grid.face_areas" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -414,28 +573,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T10:04:29.086401Z", + "start_time": "2023-06-09T10:04:29.048402Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ - "from uxarray.helpers import calculate_face_area" - ], - "metadata": { - "collapsed": false - } + "from uxarray.utils.helpers import calculate_face_area" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "`Grid.calculate_face_area` takes in three coordinate variables (x, y, z) in the form of numpy arrays and the coordinate type (either spherical or artesian) and computes the face area from the set of points" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T10:04:29.086401Z", + "start_time": "2023-06-09T10:04:29.064401Z" + } + }, "outputs": [], "source": [ "cart_x = np.array([\n", @@ -454,17 +623,40 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-09T10:04:29.095401Z", + "start_time": "2023-06-09T10:04:29.081402Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0901881354848064" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "calculate_face_area(cart_x, cart_y, cart_z, coords_type=\"cartesian\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -482,5 +674,5 @@ } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/docs/gallery.yml b/docs/gallery.yml index 2e5f35b0e..5c404552f 100644 --- a/docs/gallery.yml +++ b/docs/gallery.yml @@ -1,9 +1,9 @@ -- title: Reading In Grid Data - path: examples/001-read-grid-data.ipynb +- title: Working with Unstructured Grid Data + path: examples/001-working-with-unstructured-grids.ipynb thumbnail: _static/thumbnails/default.svg -- title: Accessing Grid Information - path: examples/002-access-grid-info.ipynb +- title: Grid Topology Overview + path: examples/002-grid-topology.ipynb thumbnail: _static/thumbnails/default.svg - title: Face Area Calculations diff --git a/docs/getting-started/freq-asked-questions.rst b/docs/getting-started/freq-asked-questions.rst new file mode 100644 index 000000000..7d3e60d95 --- /dev/null +++ b/docs/getting-started/freq-asked-questions.rst @@ -0,0 +1,7 @@ +.. currentmodule:: uxarray + +========================== +Frequently Asked Questions +========================== + +Coming soon! diff --git a/docs/getting-started/overview.rst b/docs/getting-started/overview.rst new file mode 100644 index 000000000..d3d176f65 --- /dev/null +++ b/docs/getting-started/overview.rst @@ -0,0 +1,68 @@ +.. currentmodule:: uxarray + +====================== +Overview: Why UXarray? +====================== +UXarray aims to address the geoscience community need for tools that enable standard +data analysis techniques to operate directly on unstructured grids. It extends upon +and inherits from the commonly used Xarray Python package to provide a powerful and +familiar interface for working with unstructured grids in Python. UXarray provides +Xarray styled functions to better read in and use unstructured grid datasets that +follow standard conventions, including UGRID, MPAS, SCRIP, and Exodus formats. + + +Unstructured Grids +================== +The "U" in UXarray stands for "Unstructured Grids". These types of grids differ from +typical Structured Grids in terms of complexity, requiring additional overhead to +store and represent their geometry and topology. However, these types of +grids are extremely flexible and scalable. + +UXarray uses the `UGRID `_ +conventions as a +foundation to represent Unstructured Grids. These conventions +are intended to describe how these grids should be stored within a NetCDF file, with +a particular focus on environmental and geoscience applications. We chose to use a +single convention for our grid representation instead of having separate ones for each +grid format, meaning that we encode all supported unstructured grid formats in the +UGRID conventions at the data loading step. + +Specifically, our core functionality is build around two-dimension +Unstructured Grids as defined by the 2D Flexible Mesh Topology in the +UGRID conventions, which can contain a mix of triangles, quadrilaterals, or +other geometric faces. + + + + + +Core Data Structures +==================== + +The functionality of UXarray is built around three core data structures which provide +an Unstructured Grid aware implementation of many Xarray functions and use cases. + +* ``Grid`` is used to represent our Unstructured Grid, housing grid-specific methods + and topology variables. +* ``UxDataset`` inherits from the ``xarray.Dataset`` class, providing much of the same + functionality but extended to operate on Unstructured Grids. Other than new and + overloaded methods, it is linked to a ``Grid`` object through the use of a class + property (``UxDataset.uxgrid``) to provide a grid-aware implementation. An instance + of ``UxDataset`` can be thought of as a collection of Data Variables that reside on + some Unstructured Grid as defined in the ``uxgrid`` property. +* ``UxDataArray`` similarly inherits from the ``xarray.DataArray`` class and contains + a ``Grid`` property (``UxDataArray.uxgrid``) just like ``UxDataset``. + +Core Functionality +==================== +In addition to providing a way to load in and interface with Unstructured Grids, we +also aim to provide computational and analysis operators that directly operate on +Unstructured Grids. + +The list of currently implemented operators can be found in the +`User API `_ +documentation. + +Get involved in the `Prioritization of Uxarray analysis +operators `_ to be released in +the future! diff --git a/docs/getting-started/quick-install.rst b/docs/getting-started/quick-install.rst new file mode 100644 index 000000000..56371bf44 --- /dev/null +++ b/docs/getting-started/quick-install.rst @@ -0,0 +1,26 @@ +.. currentmodule:: uxarray + +################## +Quick Installation +################## + +This quick installation guide is meant to provide a quick help to get up and running +with UXarray. For a more detailed guide, check out our Installation section. + +Required Dependencies +##################### +* Python (3.9 or later) +* xarray + + +Conda +##### +:: + + conda install -c conda-forge uxarray + +PyPi +##### +:: + + pip install uxarray diff --git a/docs/index.rst b/docs/index.rst index 3d04a04ce..72d4a2c9c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,11 +21,13 @@ .. | .. | -Uxarray Documentation +UXarray Documentation ===================== -Uxarray aims to provide xarray styled functionality for unstructured grid datasets -following ugrid conventions. +UXarray provides Xarray-styled functionality for working with unstructured grids build around the +`UGRID `_ conventions. + + .. grid:: 1 1 2 2 :gutter: 2 @@ -72,6 +74,7 @@ following ugrid conventions. :hidden: :caption: For users + ANNOUNCEMENT! Installation Getting Started Usage Examples diff --git a/docs/installation.rst b/docs/installation.rst index 49462065f..fa4c029ac 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,45 +5,45 @@ Installation ============ -This installation guide includes only the Uxarray installation instructions. Please -refer to `Uxarray Contributor's Guide `_ -for detailed information about how to contribute to the uxarray project. +This installation guide includes only the UXarray installation instructions. Please +refer to `UXarray Contributor's Guide `_ +for detailed information about how to contribute to the UXarray project. -Installing Uxarray via Conda +Installing UXarray via Conda ---------------------------- -The easiest way to install Uxarray along with its dependencies is via +The easiest way to install UXarray along with its dependencies is via `Conda `_:: conda install -c conda-forge uxarray Note that the Conda package manager automatically installs all `required` -dependencies of Uxarray, meaning it is not necessary to explicitly install -Xarray or other required packages when installing Uxarray. +dependencies of UXarray, meaning it is not necessary to explicitly install +Xarray or other required packages when installing UXarray. If you are interested in learning more about how Conda environments work, please visit the `managing environments `_ page of the Conda documentation. -Installing Uxarray via PyPI +Installing UXarray via PyPI --------------------------- An alternative to Conda is using pip:: pip install uxarray -Installing Uxarray from source (Github) +Installing UXarray from source (Github) --------------------------------------- -Installing Uxarray from source code is a fairly straightforward task, but +Installing UXarray from source code is a fairly straightforward task, but doing so should not be necessary for most users. If you `are` interested in -installing Uxarray from source, you will first need to get the latest version +installing UXarray from source, you will first need to get the latest version of the code:: git clone https://github.com/UXARRAY/uxarray.git cd uxarray -Required dependencies for installing and testing Uxarray +Required dependencies for installing and testing UXarray ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The following packages should be installed (in your active conda @@ -59,10 +59,10 @@ how to setup a conda environment with them. Creating a Conda environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The Uxarray source code includes a conda environment definition file +The UXarray source code includes a conda environment definition file (:code:`environment.yml`) in the :code:`/ci` folder under the root directory that can be used to create a development environment -containing all of the packages required to build Uxarray. The +containing all of the packages required to build UXarray. The following commands should work on Windows, Linux, and macOS to create and activate a new conda environment from that file:: @@ -73,17 +73,17 @@ Installing from source ^^^^^^^^^^^^^^^^^^^^^^ Once the dependencies listed above are installed, you can install -Uxarray with running the following command from the root-directory:: +UXarray with running the following command from the root-directory:: pip install . For compatibility purposes, we strongly recommend using Conda to configure your build environment as described above. -Testing Uxarray source +Testing UXarray source ^^^^^^^^^^^^^^^^^^^^^^ -A Uxarray code base can be tested from the root directory of the source +A UXarray code base can be tested from the root directory of the source code repository using the following command (Explicit installation of the `pytest `_ package may be required, please see above):: diff --git a/docs/internal_api/index.rst b/docs/internal_api/index.rst index 68251aef5..8b5692964 100644 --- a/docs/internal_api/index.rst +++ b/docs/internal_api/index.rst @@ -1,54 +1,154 @@ .. currentmodule:: uxarray +######## Internal API -============ +######## This page shows already-implemented Uxarray internal API functions. You can also -check the draft `Uxarray API +check the draft `UXarray API `_ documentation to see the tentative whole API and let us know if you have any feedback! -Grid Methods ------------- - -.. autosummary:: - :nosignatures: - :toctree: ./generated/ - - grid.Grid.__init_ds_var_names__ - grid.Grid.__from_ds__ - grid.Grid.__from_vert__ - grid.Grid.__init_grid_var_attrs__ - grid.Grid._build_edge_node_connectivity - grid.Grid._build_face_edges_connectivity - grid.Grid._populate_cartesian_xyz_coord - grid.Grid._populate_lonlat_coord - grid.Grid._build_nNodes_per_face - - -Grid Helper Modules --------------------- -.. autosummary:: - :nosignatures: - :toctree: ./generated/ - - _exodus._read_exodus - _exodus._encode_exodus - _exodus._get_element_type - _ugrid._encode_ugrid - _ugrid._read_ugrid - _mpas._dual_to_ugrid - _mpas._primal_to_ugrid - _mpas._replace_padding - _mpas._replace_zeros - _mpas._to_zero_index - _mpas._set_global_attrs - _mpas._read_mpas - _scrip._read_scrip - _scrip._encode_scrip - _scrip._to_ugrid - helpers._is_ugrid - helpers._convert_node_xyz_to_lonlat_rad - helpers._convert_node_lonlat_rad_to_xyz - helpers._normalize_in_place - helpers._replace_fill_values + +UxDataset +========= +The ``uxarray.UxDataset`` class inherits from ``xarray.Dataset``. Below is a list of +features explicitly added to work on Unstructured Grids. + + +Class +----- +.. autosummary:: + :toctree: _autosummary + + UxDataset + + +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + UxDataset._source_datasets + UxDataset._uxgrid + + + +Methods +------- +.. autosummary:: + :toctree: _autosummary + __getitem__ + __setitem__ + _calculate_binary_op + _construct_dataarray + _construct_direct + _copy + _replace + + + +UxDataArray +=========== +The ``uxarray.UxDataArray`` class inherits from ``xarray.DataArray``. Below is a list of +features explicitly added to work on Unstructured Grids. + +Class +----- +.. autosummary:: + :toctree: _autosummary + + UxDataArray + + +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + + UxDataArray._uxgrid + + +Methods +------- +.. autosummary:: + :toctree: _autosummary + + _construct_direct + _copy + _replace + + + +Grid +=========== + +Class +---------- +.. autosummary:: + :toctree: _autosummary + + Grid + + +IO +---------- +.. autosummary:: + :toctree: _autosummary + + io._exodus._read_exodus + io._exodus._encode_exodus + io._exodus._get_element_type + io._mpas._dual_to_ugrid + io._mpas._primal_to_ugrid + io._mpas._replace_padding + io._mpas._replace_zeros + io._mpas._to_zero_index + io._mpas._set_global_attrs + io._mpas._read_mpas + io._ugrid._encode_ugrid + io._ugrid._read_ugrid + io._scrip._read_scrip + io._scrip._encode_scrip + io._scrip._to_ugrid + + +Methods +------- +.. autosummary:: + :toctree: _autosummary + + __init_grid_var_names__ + __from_ds__ + __from_vert__ + __init_grid_var_attrs__ + _build_edge_node_connectivity + _build_face_edges_connectivity + _build_nNodes_per_face + _populate_cartesian_xyz_coord + _populate_lonlat_coord + + + +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + + Grid._Mesh2_node_z + +Operators +--------- +.. autosummary:: + :toctree: _autosummary + Grid.__eq__ + Grid.__ne__ + +Helpers +=========== + +.. currentmodule:: uxarray +.. autosummary:: + :toctree: _autosummary + + utils.helpers._is_ugrid + utils.helpers._replace_fill_values diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 24e8e172c..6e5be35a4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,4 +5,14 @@ Quick Start Guide ================= -Coming Soon! +This getting started guide aims to get you using UXarray productively as quickly as possible by covering +the main concepts. For a more detailed overview of functionality, check out our API Reference +and Usage Example sections. + +.. toctree:: + :maxdepth: 1 + :hidden: + + getting-started/overview.rst + getting-started/quick-install.rst + getting-started/freq-asked-questions.rst diff --git a/docs/user_api/index.rst b/docs/user_api/index.rst index 1c0a1970a..9fe5ddbb9 100644 --- a/docs/user_api/index.rst +++ b/docs/user_api/index.rst @@ -1,45 +1,157 @@ .. currentmodule:: uxarray +######## User API -======== +######## This page shows already-implemented Uxarray user API functions. You can also -check the draft `Uxarray API +check the draft `UXarray API `_ documentation to see the tentative whole API and let us know if you have any feedback! -Grid Class + + +UxDataset +========= +A ``xarray.Dataset``-like, multi-dimensional, in memory, array database. +Inherits from ``xarray.Dataset`` and has its own unstructured grid-aware +dataset operators and attributes through the ``uxgrid`` accessor. + +Below is a list of features explicitly added to `UxDataset` to work on +Unstructured Grids: + + +Class +----- +.. autosummary:: + :toctree: _autosummary + + UxDataset + +IO ---------- .. autosummary:: :toctree: _autosummary - grid.Grid + open_dataset + open_mfdataset +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + + UxDataset.uxgrid + UxDataset.source_datasets -Grid Methods ------------- +Methods +------- .. autosummary:: :toctree: _autosummary - grid.Grid.calculate_total_face_area - grid.Grid.compute_face_areas - grid.Grid.encode_as - grid.Grid.integrate + UxDataset.info + UxDataset.integrate + + +UxDataArray +=========== +N-dimensional ``xarray.DataArray``-like array. Inherits from `xarray.DataArray` +and has its own unstructured grid-aware array operators and attributes through +the ``uxgrid`` accessor. + +Below is a list of features explicitly added to `UxDataset` to work on +Unstructured Grids: + +Class +----- +.. autosummary:: + :toctree: _autosummary + + UxDataArray + + +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + + UxDataArray.uxgrid + -Helper Functions ----------------- + +Grid +=========== +Unstructured grid topology definition to store stores grid topology dimensions, +coordinates, variables and provides grid-specific functions. + +Can be used standalone to explore an unstructured grid topology, or can be +seen as the property of ``uxarray.UxDataset`` and ``uxarray.DataArray`` to make +them unstructured grid-aware data sets and arrays. + +Class +---------- +.. autosummary:: + :toctree: _autosummary + + Grid + +IO +---------- +.. autosummary:: + :toctree: _autosummary + + open_grid + + +Methods +------- +.. autosummary:: + :toctree: _autosummary + + Grid.calculate_total_face_area + Grid.compute_face_areas + Grid.encode_as + Grid.integrate + Grid.copy + +Attributes +---------- +.. autosummary:: + :toctree: _autosummary + + Grid.Mesh2 + Grid.parsed_attrs + Grid.nMesh2_node + Grid.nMesh2_face + Grid.nMesh2_edge + Grid.nMaxMesh2_face_nodes + Grid.nMaxMesh2_face_edges + Grid.nNodes_per_face + Grid.Mesh2_node_x + Grid.Mesh2_node_y + Grid.Mesh2_face_x + Grid.Mesh2_face_y + Grid.Mesh2_face_nodes + Grid.Mesh2_edge_nodes + Grid.Mesh2_face_edges + + +Helpers +=========== + +.. currentmodule:: uxarray .. autosummary:: :toctree: _autosummary - helpers.calculate_face_area - helpers.calculate_spherical_triangle_jacobian - helpers.calculate_spherical_triangle_jacobian_barycentric - helpers.get_all_face_area_from_coords - helpers.get_gauss_quadratureDG - helpers.get_tri_quadratureDG - helpers.grid_center_lat_lon - helpers.parse_grid_type - helpers.node_xyz_to_lonlat_rad - helpers.node_lonlat_rad_to_xyz - helpers.normalize_in_place - helpers.close_face_nodes + calculate_face_area + calculate_spherical_triangle_jacobian + calculate_spherical_triangle_jacobian_barycentric + close_face_nodes + get_all_face_area_from_coords + get_gauss_quadratureDG + get_tri_quadratureDG + grid_center_lat_lon + node_xyz_to_lonlat_rad + node_lonlat_rad_to_xyz + normalize_in_place + parse_grid_type diff --git a/docs/user_api/uxarray_api.md b/docs/user_api/uxarray_api.md index 8665ee4d0..405810ed5 100644 --- a/docs/user_api/uxarray_api.md +++ b/docs/user_api/uxarray_api.md @@ -2,205 +2,299 @@ Core (tier 1) functionality is indicated using regular text/list item style. \ Secondary (tier 2) functionality is indicated using (*) in front. -# class uxarray.Grid +# 1. class ``uxarray.UxDataset`` -Describes an unstructured grid. +The ``uxarray.UxDataset`` class inherits from ``xarray.Dataset`` +and A xarray.Dataset-like, and it is multi-dimensional, in +memory, array database. It has the ``Grid`` object, ``uxgrid``, as a +property to be unstructured grid-aware. -## uxarray.Grid Attributes +## 1.1. UxDataset IO -- uxarray.Grid.ds: DataSet\ - DataSet containing uxarray.Grid properties - `dims={nMesh2_node (total number of nodes), - nMesh2_face (number of faces), - MaxNumNodesPerFace (maximum number of nodes per face)}` \ - `optional_dims={nMesh2_edge (number of edges, optional), - MaxNumFacesPerNode (max number of faces per node),Two, Three, Four}` +- uxarray.open_dataset(grid_filename_or_obj, [kwargs]) \ + Open a single dataset, given a grid topology definition. -- (*) uxarray.Grid.islatlon: boolean \ +- uxarray.open_mfdataset(grid_filename_or_obj, paths) \ + Open multiple datasets, given a grid topology definition. + +## 1.2. UxDataset Attributes + +- UXDataset.uxgrid: ``uxarray.Grid`` \ + ``uxarray.Grid`` property to make ``UxDataset`` unstructured grid-aware + +- UxDataset.source_datasets: str \ + Property to keep track of the source data set files or object used to + instantiate this ``UxDataset`` (For diagnostics and reporting purposes). + +## 1.3. UxDataset Methods + +### 1.3.1. Implemented UxDataset Methods + +- UxDataset.info(self) \ + Concise summary of UxDataset variables and attributes + +- np.float64 UxDataset.integrate(self, [quadrature_rule, order]) \ + Integrate dataset variables over all the faces of the given mesh. + +### 1.3.2. Future UxDataset Methods + +- np.float64 UxDataset.integrate(self, [quadrature_rule, order, Grid region]) \ + Integrate the dataset variables over a region, if specified. + +- UxDataset UxDataset.regrid(self, uxarray.Grid target_grid, opts) \ + Regrid the dataset to the target_grid (by default via 1st order FV). + +- UxDataset UxDataset.zonal_mean(self, integer bin_count or bin_lats) \ + Compute global zonal means over bincount latitudinal bands. + +- (*) UxDataset uxarray.UxDataset.composite(self, nodes) \ + Produce composites of the DataArray at the specific locations via + stereographic projection. + +- (*) UxDataset UxDataset.snapshot(self, nodes) \ + Produce snapshots of the DataArray at the specific locations via + stereographic projection. + +This list will be further populated ... + +# 2. class ``uxarray.UxDataArray`` + +N-dimensional, ``xarray.DataArray``-like array. The +``uxarray.UxDataArray`` class inherits from ``xarray.DataArray``. It has +the ``Grid`` object, `uxgrid`, as a property to be unstructured grid-aware. + +## 2.1. UxDataArray Attributes + +### 2.1.1. Implemented UxDataArray Attributes + +- UXDataArray.uxgrid: ``uxarray.Grid`` \ + ``uxarray.Grid`` property to make ``UxDataArray`` unstructured grid-aware + +### 2.1.2. Future UxDataArray Attributes + +- UxDataArray.type: enumeration {“vertexcentered”, “facecentered”, + “faceaverage”, “edgecentered”, “edgeorthogonal”, “edgeparallel”, + “cgll”, “dgll”} \ + Where data is stored in this UxDataArray. + +- (*) UxDataArray.np: integer \ + Polynomial order of data (when using UxDataArray.type = “cgll” or “dgll”) + +## 2.2. UxDataArray Methods + +### 2.2.1 Implemented UxDataArray Methods + +- UxDataArray.integrate(self, [quadrature_rule, order]) \ + Integrate over all the faces of the given mesh. + +- ### 2.2.2 Future UxDataArray Methods + +- UxDataArray UxDataArray.divergence(self, UxDataArray other) \ + Compute the divergence of the vector field defined by self and other. + +- UxDataArray UxDataArray.relative_vorticity(self, UxDataArray other) \ + Compute the vertical component of the vorticity of the vector field defined + by self and other. + +- UxDataArray UxDataArray.laplacian(self) \ + Compute the scalar Laplacian of the scalar field defined by self. + +- (UxDataArray, UxDataArray) UxDataArray.gradient(self) \ + Compute the gradient of the scalar field defined by self in spherical + coordinates. + +- UxDataArray UxDataArray.scalardotgradient(self, UxDataArray v, UxDataArray q) \ + Compute the dot product between a vector field (self, v) and the gradient + of a scalar field q. + +This list will be further populated ... + +# 3. class uxarray.Grid + +Describes an unstructured grid topology. It can be used standalone to explore +an unstructured grid topology, or can be seen as the property of +``uxarray.UxDataset`` and ``uxarray.DataArray`` to make them unstructured +grid-aware data sets and arrays, respectively. + +## 3.1. Grid IO +- uxarray.open_grid(grid_filename_or_obj, gridspec, [kwargs]) \ + Create a ``Grid`` object from a grid topology definition. + +## 3.2. Grid Attributes + +### 3.2.1. Implemented Grid Attributes + +- Grid.isconcave: boolean \ + A flag indicating the grid contains concave faces. If this flag is set, + then alternative algorithms may be needed for some of the operations below. + +- Grid.islatlon: boolean \ A flag indicating the grid is a latitude longitude grid. -- (*) uxarray.Grid.isconcave: boolean \ - A flag indicating the grid contains concave faces. If - this flag is set then alternative algorithms may be needed - for some of the operations below. +- Grid.source_grid: str \ + The source file or object for this Grid's definition (For diagnostics and + reporting purposes). -- (*) uxarray.Grid.edgedual: uxarray.Grid \ - The edge dual grid. +- Grid.use_dual: boolean \ + A flag indicating if the grid is a MPAS dual mesh. -- uxarray.Grid.source_grid: string \ - The source file for this uxarray.Grid's definition (For - diagnostics and reporting purposes). +- Grid.vertices: boolean \ + A flag indicating if the grid is built via vertices. -- uxarray.Grid.source_datasets: string \ - The source file(s) for this uxarray.Grid's corresponding - data (For diagnostics and reporting purposes). +- Grid.Mesh2: np.float64 xarray.DataArray \ + UGRID Attribute. Indicates the topology data of a 2D unstructured mesh (just + like the dummy variable "Mesh2" in the UGRID conventions). -- (*) uxarray.Grid.vertexdual: uxarray.Grid \ - The vertex dual grid. +- Grid.Mesh2_face_x: np.float64 xarray.DataArray of size (nMesh2_face) \ + UGRID Coordinate Variable. 2D longitude coordinate of face centers in degrees. -According to the UGRID specification, the UGRID file should -contain a dummy variable with attribute cf_role and value -“mesh_topology”. This variable stores information on mesh -topology, including relevant variable names. The API will -need to search for the variable containing this attribute -and throw an error if it is missing. Following the UGRID -specification guide, the code below uses the name “Mesh2” -for the dummy variable, but this could be different. The -other names below are the ones used in the UGRID standards -document, but they could be different. +- Grid.Mesh2_face_y: np.float64 xarray.DataArray of size (nMesh2_face) \ + UGRID Coordinate Variable. 2D latitude coordinate of face centers in degrees. -- uxarray.Grid.Mesh2_node_x: np.float64 DataArray of size (nMesh2_node) \ - 2D longitude coordinate or 3D x coordinates for nodes on the sphere. +- Grid.Mesh2_node_x: np.float64 xarray.DataArray of size (nMesh2_node) \ + UGRID Coordinate Variable. 2D longitude coordinate for nodes on the sphere in + degrees. -- uxarray.Grid.Mesh2_node_y: np.float64 DataArray of size (nMesh2_node) \ - 2D latitude coordinate or 3D y coordinates for nodes on the sphere. +- Grid.Mesh2_node_y: np.float64 xarray.DataArray of size (nMesh2_node) \ + UGRID Coordinate Variable. 2D latitude coordinates for nodes on the sphere in + degrees. -- uxarray.Grid.Mesh2_node_z: np.float64 DataArray of size (nMesh2_node) \ - (optional) - 3D z coordinates for nodes on the sphere. +- Grid.Mesh2_node_cart_x: np.float64 xarray.DataArray of size (nMesh2_node) \ + Coordinate Variable. x coordinates for nodes in meters. -- (*) uxarray.Grid.Mesh2_node_coordinates: np.float64 DataArray of size - (nMesh2_node, Two or Three) \ - Alternative storage mechanism for node information. +- Grid.Mesh2_node_cart_y: np.float64 xarray.DataArray of size (nMesh2_node) \ + Coordinate Variable. y coordinates for nodes in meters. -- uxarray.Grid.Mesh2_face_nodes: integer DataArray of size - (nMesh2_face, MaxNumNodesPerFace) \ - A DataArray of indices for each face node, corresponding to coordinates - in uxarray.Grid.node_*. Faces can have arbitrary length, with - _FillValue=-1 used when faces have fewer nodes than MaxNumNodesPerFace. - Nodes are in counter-clockwise order. +- Grid.Mesh2_node_cart_z: np.float64 xarray.DataArray of size (nMesh2_node) \ + Coordinate Variable. z coordinates for nodes in meters. -- uxarray.Grid.Mesh2_edge_nodes: integer DataArray of size (nMesh2_edge, Two) +- Grid.nMaxMesh2_face_nodes: int + UGRID Dimension. Represents the maximum number of nodes that a face may contain. + +- Grid.nMaxMesh2_face_edges: int + Dimension. Represents the maximum number of edges per face. + +- Grid.nMesh2_edge: int + UGRID Dimension. Represents the total number of edges. + +- Grid.nMesh2_face: int + UGRID Dimension. Represents the total number of faces. + +- Grid.nMesh2_node: int + UGRID Dimension. Represents the total number of nodes. + +- Grid.nNodes_per_face: int + Dimension. Represents the number of non-fill-value nodes per face. + +- Grid.Mesh2_edge_nodes: int xarray.DataArray of size (nMesh2_edge, Two) (optional) \ - A DataArray of indices for each edge. Nodes are in arbitrary order. + UGRID Connectivity Variable. Maps every edge to the two nodes that it connects + +- Grid.Mesh2_face_edges: int xarray.DataArray of size (nMesh2_face, + nMaxMesh2_face_nodes) (optional) \ + UGRID Connectivity Variable. Maps every face to its edges. + +- Grid.Mesh2_face_nodes: int xarray.DataArray of size + (nMesh2_face, MaxNumNodesPerFace) \ + UGRID Connectivity Variable. Maps each face to its corner nodes. + +- Grid.face_areas: np.float64 xarray.DataArray of size (nMesh2_face) \ + Provides areas for each face. + +- Grid.parsed_attrs: dict + Dictionary of parsed attributes from the source grid. + +### 3.2.2. Future Grid Attributes -- (*) uxarray.Grid.Mesh2_edge_types: integer DataArray of size (nMesh2_edge) +- Grid.Mesh2_node_z: np.float64 xarray.DataArray of size (nMesh2_node) \ + (optional) + 3D z coordinates for nodes on the sphere. + +- (*) Grid.edge_dual: Grid \ + The edge dual grid. + +- (*) Grid.vertex_dual: Grid \ + The vertex dual grid. + +- (*) Grid.Mesh2_edge_types: int DataArray of size (nMesh2_edge) (optional; not in UGRID standard) \ A DataArray indicating the type of edge (0 = great circle arc, 1 = line of constant latitude) -- uxarray.Grid.Mesh2_face_areas: np.float64 DataArray of size (nMesh2_face) - (optional; not in UGRID standard) \ - A DataArray providing face areas for each face. - -- (*) uxarray.Grid.Mesh2_imask: integer DataArray of size (nMesh2_face) +- (*) Grid.Mesh2_imask: int DataArray of size (nMesh2_face) (optional; not in UGRID standard) \ - The integer mask for this grid (1 = face is active; 0 = face is inactive) + The int mask for this grid (1 = face is active; 0 = face is inactive) -- uxarray.Grid.Mesh2_face_edges: integer DataArray of size (nMesh2_face, - MaxNumNodesPerFace) (optional) \ - A DataArray of indices indicating edges that are neighboring each face. - -- uxarray.Grid.Mesh2_face_links: integer DataArray of size (nMesh2_face, +- Grid.Mesh2_face_links: int DataArray of size (nMesh2_face, MaxNumNodesPerFace) (optional) \ A DataArray of indices indicating faces that are neighboring each face. -- uxarray.Grid.Mesh2_edge_faces: integer DataArray of size (nMesh2_edge, +- Grid.Mesh2_edge_faces: int DataArray of size (nMesh2_edge, Two) (optional) \ A DataArray of indices indicating faces that are neighboring each edge. -- uxarray.Grid.Mesh2_node_faces: integer DataArray of size (nMesh2_node, +- Grid.Mesh2_node_faces: int DataArray of size (nMesh2_node, MaxNumFacesPerNode) (optional) \ A DataArray of indices indicating faces that are neighboring each node. -- (*) uxarray.Grid.Mesh2_latlon_bounds: np.float64 DataArray of size +- (*) Grid.Mesh2_latlon_bounds: np.float64 DataArray of size (nMesh2_face, Four) (optional; not in UGRID standard) \ A DataArray of values indicating the latitude-longitude boundaries of each face. -- (*) uxarray.Grid.Mesh2_overlapfaces_a: integer DataArray of size +- (*) Grid.Mesh2_overlapfaces_a: int DataArray of size (nMesh2_face) (optional; not in UGRID standard) \ A DataArray of indices storing the indices of the parent face from grid A, available when this Grid is a supermesh. -- (*) uxarray.Grid.Mesh2_overlapfaces_b: integer DataArray of size +- (*) Grid.Mesh2_overlapfaces_b: int DataArray of size (nMesh2_face) (optional; not in UGRID standard) \ A DataArray of indices storing the indices of the parent face from grid B, available when this Grid is a supermesh. -## uxarray.Grid Functions +## 3.3. Grid Methods -- uxarray.Grid.__init__(self, xarray dataset) \ - Populate Grid object with Xarray dataset. The routine will automatically - detect if it is a UGrid, SCRIP, Exodus, or shape file. +### 3.3.1. Implemented Grid Methods -- (*) uxarray.Grid.__init__(self, string gridspec) \ - Define a grid specified by gridspec string (analogous to the gridspec - used in ncremap for grid generation). +- Grid.__init__(self, input_obj) \ + Populate Grid object with input_object that can be one of xarray.Dataset, + ndarray, list, or tuple. The routine will automatically recognize if it is + a UGRID, MPAS, SCRIP, or Exodus, or shape file. -- uxarray.Grid.__init__(self, np.float64.list vertices) \ - Create a grid with one face with vertices specified by the given argument. +- Grid.copy(self) \ + Return a deep copy of self. -- uxarray.Grid.encode_as(self, string grid_type) \ +- Grid.encode_as(self, str grid_type) \ Encode a `uxarray.Grid` as a `xarray.Dataset`in the specified grid type - (UGRID, SCRIP, Exodus, or SHP). + (UGRID, SCRIP, Exodus). -- uxarray.Grid.calculatefaceareas(self) \ - Calculate the area of all faces. +- Grid.calculate_total_face_area(self, [quadrature_rule, order]) \ + Calculate the total surface area of the whole mesh, i.e. sum of face areas. -- uxarray.Grid.build_node_face_connectivity(self) \ - Build the node-face connectivity array. - -- uxarray.Grid.build_edge_face_connectivity(self) \ - Build the edge-face connectivity array. - -- uxarray.Grid.buildlatlon_bounds(self) \ - Build the array of latitude-longitude bounding boxes. - -- uxarray.Grid.validate(self) \ - Validate that the grid conforms to the UGRID standards. - -## Additional xarray.DataArray Attributes - -- xarray.DataArray.grid: uxarray.Grid \ - The grid associated with this xarray.DataArray. - -- xarray.type: enumeration {“vertexcentered”, “facecentered”, - “faceaverage”, “edgecentered”, “edgeorthogonal”, “edgeparallel”, - “cgll”, “dgll”} \ - Where data is stored in this DataArray. +- Grid.compute_face_areas(self, [quadrature_rule, order]) \ + Calculate the individual areas of all faces. -- (*) xarray.np: integer \ - Polynomial order of data (when using xarray.type = “cgll” or “dgll”) +### 3.3.2. Future Grid Methods -## Helper Functions - -- np.float64 uxarray.integrate(self, xarray.DataArray q, - uxarray.Grid region (optional)) \ - Integrate the DataArray globally or over a specified region - (if specified). - -- xarray.DataSet uxarray.zonalmean(self, xarray.DataArray q, - integer bincount or binlats) \ - Compute global zonal means over bincount latitudinal bands. - -- xarray.DataSet uxarray.regrid(self, xarray.DataArray q, - uxarray.Grid targetgrid, opts) \ - Regrid the data to the target grid (by default via 1st order FV). - -- xarray.DataArray uxarray.divergence(xarray.DataArray u, - xarray.DataArray v) \ - Compute the divergence of the vector field defined by u and v. - -- xarray.DataArray uxarray.relative_vorticity(xarray.DataArray u, - xarray.DataArray v) \ - Compute the vertical component of the vorticity of the vector field defined by u and v. +- (*) Grid.__init__(self, str gridspec) \ + Define a grid specified by gridspec str (analogous to the gridspec + used in ncremap for grid generation). -- xarray.DataArray uxarray.laplacian(xarray.DataArray q) \ - Compute the scalar Laplacian of the scalar field q. +- Grid.build_node_face_connectivity(self) \ + Build the node-face connectivity array. -- (xarray.DataArray, xarray.DataArray) uxarray.gradient(xarray.DataArray q) \ - Compute the gradient of the scalar field q in spherical coordinates. +- Grid.build_edge_face_connectivity(self) \ + Build the edge-face connectivity array. -- xarray.DataArray uxarray.scalardotgradient(xarray.DataArray u, - xarray.DataArray v, xarray.DataArray q) \ - Compute the dot product between a vector field (u,v) and the gradient of a scalar field q. +- Grid.build_lat_lon_bounds(self) \ + Build the array of latitude-longitude bounding boxes. -- (*) xarray.Grid uxarray.supermesh(uxarray.Grid a, uxarray.Grid b) \ - Construct the supermesh, consisting of all face edges from Grids a and b. +- Grid.encode_as(self, str grid_type) \ + Encode a `uxarray.Grid` as a `xarray.Dataset`in the MPAS or SHP grid type. -- (*) xarray.DataSet xarray.DataSet.snapshot(self, nodes) \ - Produce snapshots of the DataArray at the specific locations via stereographic projection. +- Grid.validate(self) \ + Validate that the grid conforms to the UGRID standards. -- (*) xarray.DataSet xarray.DataSet.composite(self, nodes) \ - Produce composites of the DataArray at the specific locations via stereographic projection. +- (*) Grid Grid.super_mesh(self, Grid other) \ + Construct the super mesh, consisting of all face edges from Grids self and + other. diff --git a/meta.yaml b/meta.yaml index 662dd6381..561cb8c7e 100644 --- a/meta.yaml +++ b/meta.yaml @@ -1,4 +1,4 @@ -{% set version = "2023.05.0" %} +{% set version = "2023.05.0dev" %} package: name: 'uxarray' diff --git a/test/constants.py b/test/constants.py index 9132ead37..acf33feca 100644 --- a/test/constants.py +++ b/test/constants.py @@ -5,7 +5,7 @@ NNODES_outCSne8 = 386 NNODES_outCSne30 = 5402 NNODES_outRLL1deg = 64442 -DATAVARS_outCSne30 = 2 +DATAVARS_outCSne30 = 3 TRI_AREA = 1.047 # 4*Pi is 12.56 MESH30_AREA = 12.566 diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 000000000..e6b05698c --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,130 @@ +import os +from unittest import TestCase +from pathlib import Path +import numpy.testing as nt + +import uxarray as ux + +try: + import constants +except ImportError: + from . import constants + +current_path = Path(os.path.dirname(os.path.realpath(__file__))) + + +class TestAPI(TestCase): + + geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" + gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" + geoflow_data_v1 = geoflow_data_path / "v1.nc" + geoflow_data_v2 = geoflow_data_path / "v2.nc" + geoflow_data_v3 = geoflow_data_path / "v3.nc" + + gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" + dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" + dsfiles_mf_ne30 = str( + current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" + + def test_open_geoflow_dataset(self): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call.""" + + # Paths to Data Variable files + data_paths = [ + self.geoflow_data_v1, self.geoflow_data_v2, self.geoflow_data_v3 + ] + + uxds_v1 = ux.open_dataset(self.gridfile_geoflow, data_paths[0]) + + # Ideally uxds_v1.uxgrid should NOT be None + with self.assertRaises(AssertionError): + nt.assert_equal(uxds_v1.uxgrid, None) + + def test_open_dataset(self): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call.""" + + uxds_var2_ne30 = ux.open_dataset(self.gridfile_ne30, + self.dsfile_var2_ne30) + + nt.assert_equal(uxds_var2_ne30.uxgrid.Mesh2_node_x.size, + constants.NNODES_outCSne30) + nt.assert_equal(len(uxds_var2_ne30.uxgrid._ds.data_vars), + constants.DATAVARS_outCSne30) + nt.assert_equal(uxds_var2_ne30.uxgrid.source_grid, + str(self.gridfile_ne30)) + nt.assert_equal(uxds_var2_ne30.source_datasets, + str(self.dsfile_var2_ne30)) + + def test_open_mf_dataset(self): + """Loads multiple datasets with their grid topology file using + uxarray's open_dataset call.""" + + uxds_mf_ne30 = ux.open_mfdataset(self.gridfile_ne30, + self.dsfiles_mf_ne30) + + nt.assert_equal(uxds_mf_ne30.uxgrid.Mesh2_node_x.size, + constants.NNODES_outCSne30) + nt.assert_equal(len(uxds_mf_ne30.uxgrid._ds.data_vars), + constants.DATAVARS_outCSne30) + nt.assert_equal(uxds_mf_ne30.uxgrid.source_grid, + str(self.gridfile_ne30)) + nt.assert_equal(uxds_mf_ne30.source_datasets, self.dsfiles_mf_ne30) + + def test_open_grid(self): + """Loads only a grid topology file using uxarray's open_grid call.""" + uxgrid = ux.open_grid(self.gridfile_geoflow) + + nt.assert_almost_equal(uxgrid.calculate_total_face_area(), + constants.MESH30_AREA, + decimal=3) + + def test_copy_dataset(self): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call and make a copy of the object.""" + + uxds_var2_ne30 = ux.open_dataset(self.gridfile_ne30, + self.dsfile_var2_ne30) + + # make a shallow and deep copy of the dataset object + uxds_var2_ne30_copy_deep = uxds_var2_ne30.copy(deep=True) + uxds_var2_ne30_copy = uxds_var2_ne30.copy(deep=False) + + # Ideally uxds_var2_ne30_copy.uxgrid should NOT be None + with self.assertRaises(AssertionError): + nt.assert_equal(uxds_var2_ne30_copy.uxgrid, None) + + # Check that the copy is a shallow copy + assert (uxds_var2_ne30_copy.uxgrid is uxds_var2_ne30.uxgrid) + assert (uxds_var2_ne30_copy.uxgrid == uxds_var2_ne30.uxgrid) + + # Check that the deep copy is a deep copy + assert (uxds_var2_ne30_copy_deep.uxgrid == uxds_var2_ne30.uxgrid) + assert (uxds_var2_ne30_copy_deep.uxgrid is not uxds_var2_ne30.uxgrid) + + def test_copy_dataarray(self): + """Loads an unstructured grid and data using uxarray's open_dataset + call and make a copy of the dataarray object.""" + + # Paths to Data Variable files + data_paths = [ + self.geoflow_data_v1, self.geoflow_data_v2, self.geoflow_data_v3 + ] + + uxds_v1 = ux.open_dataset(self.gridfile_geoflow, data_paths[0]) + + # get the uxdataarray object + v1_uxdata_array = uxds_v1['v1'] + + # make a shallow and deep copy of the dataarray object + v1_uxdata_array_copy_deep = v1_uxdata_array.copy(deep=True) + v1_uxdata_array_copy = v1_uxdata_array.copy(deep=False) + + # Check that the copy is a shallow copy + assert (v1_uxdata_array_copy.uxgrid is v1_uxdata_array.uxgrid) + assert (v1_uxdata_array_copy.uxgrid == v1_uxdata_array.uxgrid) + + # Check that the deep copy is a deep copy + assert (v1_uxdata_array_copy_deep.uxgrid == v1_uxdata_array.uxgrid) + assert (v1_uxdata_array_copy_deep.uxgrid is not v1_uxdata_array.uxgrid) diff --git a/test/test_dataset.py b/test/test_dataset.py new file mode 100644 index 000000000..a8b79b281 --- /dev/null +++ b/test/test_dataset.py @@ -0,0 +1,55 @@ +import os +from unittest import TestCase +from pathlib import Path +import numpy.testing as nt + +import uxarray as ux + +try: + import constants +except ImportError: + from . import constants + +current_path = Path(os.path.dirname(os.path.realpath(__file__))) + +gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" +dsfiles_mf_ne30 = str( + current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" + +gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" +dsfile_v1_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" + + +class TestUxDataset(TestCase): + + def test_uxgrid_setget(self): + """Load a dataset with its grid topology file using uxarray's + open_dataset call and check its grid object.""" + + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + + uxgrid_var2_ne30 = ux.open_grid(gridfile_ne30) + assert (uxds_var2_ne30.uxgrid == uxgrid_var2_ne30) + + def test_integrate(self): + """Load a dataset and calculate integrate().""" + + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + + integrate_var2 = uxds_var2_ne30.integrate() + + nt.assert_almost_equal(integrate_var2, constants.VAR2_INTG, decimal=3) + + def test_info(self): + """Tests custom info containing grid information.""" + uxds_var2_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + + import contextlib + import io + + with contextlib.redirect_stdout(io.StringIO()): + try: + uxds_var2_geoflow.info(show_attrs=True) + except Exception as exc: + assert False, f"'uxds_var2_geoflow.info()' raised an exception: {exc}" diff --git a/test/test_exodus.py b/test/test_exodus.py index 46a40ec4f..dda8c6603 100644 --- a/test/test_exodus.py +++ b/test/test_exodus.py @@ -4,27 +4,27 @@ from unittest import TestCase from pathlib import Path -import xarray as xr import uxarray as ux -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE current_path = Path(os.path.dirname(os.path.realpath(__file__))) class TestExodus(TestCase): + exo_filename = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" + exo2_filename = current_path / "meshfiles" / "exodus" / "mixed" / "mixed.exo" + def test_read_exodus(self): """Read an exodus file and writes a exodus file.""" - exo2_filename = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - xr_exo_ds = xr.open_dataset(exo2_filename) - tgrid = ux.Grid(xr_exo_ds) + uxgrid = ux.open_grid(self.exo_filename) def test_init_verts(self): """Create a uxarray grid from vertices and saves a 1 face exodus file.""" verts = [[[0, 0], [2, 0], [0, 2], [2, 2]]] - vgrid = ux.Grid(verts) + uxgrid = ux.open_grid(verts) def test_encode_exodus(self): """Read a UGRID dataset and encode that as an Exodus format.""" @@ -33,21 +33,16 @@ def test_mixed_exodus(self): """Read/write an exodus file with two types of faces (triangle and quadrilaterals) and writes a ugrid file.""" - exo2_filename = current_path / "meshfiles" / "exodus" / "mixed" / "mixed.exo" - xr_exo_ds = xr.open_dataset(exo2_filename) - tgrid = ux.Grid(xr_exo_ds) - outfile = current_path / "write_test_mixed.ug" - tgrid.encode_as("ugrid") - outfile = current_path / "write_test_mixed.exo" - tgrid.encode_as("exodus") + uxgrid = ux.open_grid(self.exo2_filename) + + uxgrid.encode_as("ugrid") + uxgrid.encode_as("exodus") def test_standardized_dtype_and_fill(self): """Test to see if Mesh2_Face_Nodes uses the expected integer datatype and expected fill value as set in constants.py.""" - exo2_filename = current_path / "meshfiles" / "exodus" / "mixed" / "mixed.exo" - xr_exo_ds = xr.open_dataset(exo2_filename) - ux_grid = ux.Grid(xr_exo_ds) + uxgrid = ux.open_grid(self.exo2_filename) - assert ux_grid.Mesh2_face_nodes.dtype == INT_DTYPE - assert ux_grid.Mesh2_face_nodes._FillValue == INT_FILL_VALUE + assert uxgrid.Mesh2_face_nodes.dtype == INT_DTYPE + assert uxgrid.Mesh2_face_nodes._FillValue == INT_FILL_VALUE diff --git a/test/test_grid.py b/test/test_grid.py index d4d0d512b..e5fc8da0b 100644 --- a/test/test_grid.py +++ b/test/test_grid.py @@ -1,13 +1,12 @@ import os import numpy as np +import numpy.testing as nt import xarray as xr from unittest import TestCase from pathlib import Path -import xarray as xr import uxarray as ux -import numpy.testing as nt try: import constants @@ -16,40 +15,44 @@ current_path = Path(os.path.dirname(os.path.realpath(__file__))) +gridfile_CSne8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" +gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" +gridfile_RLL10deg_CSne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" +gridfile_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +gridfile_fesom = current_path / "meshfiles" / "ugrid" / "fesom" / "fesom.mesh.diag.nc" +gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" + +dsfile_vortex_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" +dsfile_var2_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" + +shp_filename = current_path / "meshfiles" / "shp" / "grid_fire.shp" + class TestGrid(TestCase): - ug_filename1 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - xr_ds1 = xr.open_dataset(ug_filename1) - xr_ds2 = xr.open_dataset(ug_filename2) - xr_ds3 = xr.open_dataset(ug_filename3) - tgrid1 = ux.Grid(xr_ds1) - tgrid2 = ux.Grid(xr_ds2) - tgrid3 = ux.Grid(xr_ds3) + grid_CSne30 = ux.open_grid(gridfile_CSne30) + grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) + grid_RLL10deg_CSne4 = ux.open_grid(gridfile_RLL10deg_CSne4) def test_encode_as(self): """Reads a ugrid file and encodes it as `xarray.Dataset` in various types.""" - self.tgrid1.encode_as("ugrid") - self.tgrid2.encode_as("ugrid") - self.tgrid3.encode_as("ugrid") + self.grid_CSne30.encode_as("ugrid") + self.grid_RLL1deg.encode_as("ugrid") + self.grid_RLL10deg_CSne4.encode_as("ugrid") - self.tgrid1.encode_as("exodus") - self.tgrid2.encode_as("exodus") - self.tgrid3.encode_as("exodus") + self.grid_CSne30.encode_as("exodus") + self.grid_RLL1deg.encode_as("exodus") + self.grid_RLL10deg_CSne4.encode_as("exodus") def test_open_non_mesh2_write_exodus(self): """Loads grid files of different formats using uxarray's open_dataset call.""" - path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - xr_grid = xr.open_dataset(path) - grid = ux.Grid(xr_grid) + grid_geoflow = ux.open_grid(gridfile_CSne30) - grid.encode_as("exodus") + grid_geoflow.encode_as("exodus") def test_init_verts(self): """Create a uxarray grid from multiple face vertices with duplicate @@ -106,10 +109,10 @@ def test_init_verts(self): # Now consturct the grid using the faces_coords verts_cart = np.array(faces_coords) - vgrid = ux.Grid(verts_cart, - vertices=True, - islatlon=False, - concave=False) + vgrid = ux.open_grid(verts_cart, + vertices=True, + islatlon=False, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 6) @@ -121,10 +124,10 @@ def test_init_verts(self): np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]]) ]) - vgrid = ux.Grid(faces_verts_one, - vertices=True, - islatlon=True, - concave=False) + vgrid = ux.open_grid(faces_verts_one, + vertices=True, + islatlon=True, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 1) assert (vgrid.nMesh2_node == 6) @@ -134,10 +137,10 @@ def test_init_verts(self): faces_verts_single_face = np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]]) - vgrid = ux.Grid(faces_verts_single_face, - vertices=True, - islatlon=True, - concave=False) + vgrid = ux.open_grid(faces_verts_single_face, + vertices=True, + islatlon=True, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 1) assert (vgrid.nMesh2_node == 6) @@ -159,10 +162,11 @@ def test_init_verts_different_input_datatype(self): np.array([[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], [85, 10]]), ]) - vgrid = ux.Grid(faces_verts_ndarray, - vertices=True, - islatlon=True, - concave=False) + vgrid = ux.open_grid(faces_verts_ndarray, + vertices=True, + islatlon=True, + isconcave=False) + assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 3) assert (vgrid.nMesh2_node == 14) @@ -175,10 +179,10 @@ def test_init_verts_different_input_datatype(self): [100, 30], [105, 20]], [[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], [85, 10]]] - vgrid = ux.Grid(faces_verts_list, - vertices=True, - islatlon=False, - concave=False) + vgrid = ux.open_grid(faces_verts_list, + vertices=True, + islatlon=False, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 3) assert (vgrid.nMesh2_node == 14) @@ -190,10 +194,10 @@ def test_init_verts_different_input_datatype(self): ((125, 20), (135, 30), (125, 60), (110, 60), (100, 30), (105, 20)), ((95, 10), (105, 20), (100, 30), (85, 30), (75, 20), (85, 10)) ] - vgrid = ux.Grid(faces_verts_tuples, - vertices=True, - islatlon=False, - concave=False) + vgrid = ux.open_grid(faces_verts_tuples, + vertices=True, + islatlon=False, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 3) assert (vgrid.nMesh2_node == 14) @@ -208,115 +212,122 @@ def test_init_verts_fill_values(self): [[95, 10], [105, 20], [100, 30], [85, 30], [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]]] - vgrid = ux.Grid(faces_verts_filled_values, - vertices=True, - islatlon=False, - concave=False) + vgrid = ux.open_grid(faces_verts_filled_values, + vertices=True, + islatlon=False, + isconcave=False) assert (vgrid.source_grid == "From vertices") assert (vgrid.nMesh2_face == 3) assert (vgrid.nMesh2_node == 12) - def test_init_grid_var_attrs(self): - """Tests to see if accessing variables through set attributes is equal + def test_grid_properties(self): + """Tests to see if accessing variables through set properties is equal to using the dict.""" # Dataset with standard UGRID variable names # Coordinates xr.testing.assert_equal( - self.tgrid1.Mesh2_node_x, - self.tgrid1.ds[self.tgrid1.ds_var_names["Mesh2_node_x"]]) + self.grid_CSne30.Mesh2_node_x, self.grid_CSne30._ds[ + self.grid_CSne30.grid_var_names["Mesh2_node_x"]]) xr.testing.assert_equal( - self.tgrid1.Mesh2_node_y, - self.tgrid1.ds[self.tgrid1.ds_var_names["Mesh2_node_y"]]) + self.grid_CSne30.Mesh2_node_y, self.grid_CSne30._ds[ + self.grid_CSne30.grid_var_names["Mesh2_node_y"]]) # Variables xr.testing.assert_equal( - self.tgrid1.Mesh2_face_nodes, - self.tgrid1.ds[self.tgrid1.ds_var_names["Mesh2_face_nodes"]]) + self.grid_CSne30.Mesh2_face_nodes, self.grid_CSne30._ds[ + self.grid_CSne30.grid_var_names["Mesh2_face_nodes"]]) # Dimensions - n_nodes = self.tgrid1.Mesh2_node_x.shape[0] - n_faces, n_face_nodes = self.tgrid1.Mesh2_face_nodes.shape + n_nodes = self.grid_CSne30.Mesh2_node_x.shape[0] + n_faces, n_face_nodes = self.grid_CSne30.Mesh2_face_nodes.shape - self.assertEqual(n_nodes, self.tgrid1.nMesh2_node) - self.assertEqual(n_faces, self.tgrid1.nMesh2_face) - self.assertEqual(n_face_nodes, self.tgrid1.nMaxMesh2_face_nodes) + self.assertEqual(n_nodes, self.grid_CSne30.nMesh2_node) + self.assertEqual(n_faces, self.grid_CSne30.nMesh2_face) + self.assertEqual(n_face_nodes, self.grid_CSne30.nMaxMesh2_face_nodes) # xr.testing.assert_equal( # self.tgrid1.nMesh2_node, - # self.tgrid1.ds[self.tgrid1.ds_var_names["nMesh2_node"]]) + # self.tgrid1._ds[self.tgrid1.grid_var_names["nMesh2_node"]]) # xr.testing.assert_equal( # self.tgrid1.nMesh2_face, - # self.tgrid1.ds[self.tgrid1.ds_var_names["nMesh2_face"]]) + # self.tgrid1._ds[self.tgrid1.grid_var_names["nMesh2_face"]]) # Dataset with non-standard UGRID variable names - path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - xr_grid = xr.open_dataset(path) - grid = ux.Grid(xr_grid) - xr.testing.assert_equal(grid.Mesh2_node_x, - grid.ds[grid.ds_var_names["Mesh2_node_x"]]) - xr.testing.assert_equal(grid.Mesh2_node_y, - grid.ds[grid.ds_var_names["Mesh2_node_y"]]) + grid_geoflow = ux.open_grid(gridfile_geoflow) + + xr.testing.assert_equal( + grid_geoflow.Mesh2_node_x, + grid_geoflow._ds[grid_geoflow.grid_var_names["Mesh2_node_x"]]) + xr.testing.assert_equal( + grid_geoflow.Mesh2_node_y, + grid_geoflow._ds[grid_geoflow.grid_var_names["Mesh2_node_y"]]) # Variables - xr.testing.assert_equal(grid.Mesh2_face_nodes, - grid.ds[grid.ds_var_names["Mesh2_face_nodes"]]) + xr.testing.assert_equal( + grid_geoflow.Mesh2_face_nodes, + grid_geoflow._ds[grid_geoflow.grid_var_names["Mesh2_face_nodes"]]) # Dimensions - n_nodes = grid.Mesh2_node_x.shape[0] - n_faces, n_face_nodes = grid.Mesh2_face_nodes.shape + n_nodes = grid_geoflow.Mesh2_node_x.shape[0] + n_faces, n_face_nodes = grid_geoflow.Mesh2_face_nodes.shape - self.assertEqual(n_nodes, grid.nMesh2_node) - self.assertEqual(n_faces, grid.nMesh2_face) - self.assertEqual(n_face_nodes, grid.nMaxMesh2_face_nodes) + self.assertEqual(n_nodes, grid_geoflow.nMesh2_node) + self.assertEqual(n_faces, grid_geoflow.nMesh2_face) + self.assertEqual(n_face_nodes, grid_geoflow.nMaxMesh2_face_nodes) def test_read_shpfile(self): """Reads a shape file and write ugrid file.""" - with self.assertRaises(RuntimeError): - shp_filename = current_path / "meshfiles" / "shp" / "grid_fire.shp" - tgrid = ux.Grid(str(shp_filename)) + with self.assertRaises(ValueError): + grid_shp = ux.open_grid(shp_filename) def test_read_scrip(self): """Reads a scrip file.""" - scrip_8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" - ug_30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - # Test read from scrip and from ugrid for grid class - xr_grid_s8 = xr.open_dataset(scrip_8) - ux_grid_s8 = ux.Grid(xr_grid_s8) # tests from scrip + grid_CSne8 = ux.open_grid(gridfile_CSne8) # tests from scrip + - xr_grid_u30 = xr.open_dataset(ug_30) - ux_grid_u30 = ux.Grid(xr_grid_u30) # tests from ugrid +class TestOperators(TestCase): + grid_CSne30_01 = ux.open_grid(gridfile_CSne30) + grid_CSne30_02 = ux.open_grid(gridfile_CSne30) + grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) + def test_eq(self): + """Test Equals ('==') operator.""" + assert self.grid_CSne30_01 == self.grid_CSne30_02 -class TestIntegrate(TestCase): - mesh_file30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - data_file30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" - data_file30_v2 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" + def test_ne(self): + """Test Not Equals ('!=') operator.""" + assert self.grid_CSne30_01 != self.grid_RLL1deg + + +class TestFaceAreas(TestCase): + + grid_CSne30 = ux.open_grid(gridfile_CSne30) def test_calculate_total_face_area_triangle(self): """Create a uxarray grid from vertices and saves an exodus file.""" + verts = [[[0.57735027, -5.77350269e-01, -0.57735027], [0.57735027, 5.77350269e-01, -0.57735027], [-0.57735027, 5.77350269e-01, -0.57735027]]] - # load grid - vgrid = ux.Grid(verts, vertices=True, islatlon=False, concave=False) + grid_verts = ux.open_grid(verts, + vertices=True, + islatlon=False, + isconcave=False) #calculate area - area_gaussian = vgrid.calculate_total_face_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) - area_triangular = vgrid.calculate_total_face_area( + area_triangular = grid_verts.calculate_total_face_area( quadrature_rule="triangular", order=4) nt.assert_almost_equal(area_triangular, constants.TRI_AREA, decimal=1) def test_calculate_total_face_area_file(self): """Create a uxarray grid from vertices and saves an exodus file.""" - xr_grid = xr.open_dataset(str(self.mesh_file30)) - grid = ux.Grid(xr_grid) - - area = grid.calculate_total_face_area() + area = self.grid_CSne30.calculate_total_face_area() nt.assert_almost_equal(area, constants.MESH30_AREA, decimal=3) @@ -325,9 +336,8 @@ def test_calculate_total_face_area_sphere(self): sphere, with an expected total face area of 4pi.""" mpas_grid_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' - ds = xr.open_dataset(mpas_grid_path) - primal_grid = ux.Grid(ds, use_dual=False) - dual_grid = ux.Grid(ds, use_dual=True) + primal_grid = ux.open_grid(mpas_grid_path, use_dual=False) + dual_grid = ux.open_grid(mpas_grid_path, use_dual=True) primal_face_area = primal_grid.calculate_total_face_area() dual_face_area = dual_grid.calculate_total_face_area() @@ -340,38 +350,31 @@ def test_calculate_total_face_area_sphere(self): constants.UNIT_SPHERE_AREA, decimal=3) - def test_integrate(self): - xr_grid = xr.open_dataset(self.mesh_file30) - xr_psi = xr.open_dataset(self.data_file30) - xr_v2 = xr.open_dataset(self.data_file30_v2) - - u_grid = ux.Grid(xr_grid) - - integral_psi = u_grid.integrate(xr_psi) - integral_var2 = u_grid.integrate(xr_v2) - - nt.assert_almost_equal(integral_psi, constants.PSI_INTG, decimal=3) - nt.assert_almost_equal(integral_var2, constants.VAR2_INTG, decimal=3) - - -class TestFaceAreas(TestCase): + # TODO: Will depend on the decision for whether to provide integrate function + # from within `Grid` as well as UxDataset + # def test_integrate(self): + # xr_psi = xr.open_dataset(dsfile_vortex_CSne30) + # xr_v2 = xr.open_dataset(dsfile_var2_CSne30) + # + # integral_psi = self.grid_CSne30.integrate(xr_psi) + # integral_var2 = self.grid_CSne30.integrate(xr_v2) + # + # nt.assert_almost_equal(integral_psi, constants.PSI_INTG, decimal=3) + # nt.assert_almost_equal(integral_var2, constants.VAR2_INTG, decimal=3) def test_compute_face_areas_geoflow_small(self): """Checks if the GeoFlow Small can generate a face areas output.""" - geoflow_small_grid = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - grid_1_ds = xr.open_dataset(geoflow_small_grid) - grid_1 = ux.Grid(grid_1_ds) - grid_1.compute_face_areas() + grid_geoflow = ux.open_grid(gridfile_geoflow) + + grid_geoflow.compute_face_areas() - # removed test until fix to tranposed face nodes + # TODO: Add this test after fix to tranposed face nodes # def test_compute_face_areas_fesom(self): # """Checks if the FESOM PI-Grid Output can generate a face areas # output.""" + # grid_fesom = ux.open_grid(gridfile_fesom) # - # fesom_grid_small = current_path / "meshfiles" / "ugrid" / "fesom" / "fesom.mesh.diag.nc" - # grid_2_ds = xr.open_dataset(fesom_grid_small) - # grid_2 = ux.Grid(grid_2_ds) - # grid_2.compute_face_areas() + # grid_fesom.compute_face_areas() def test_verts_calc_area(self): faces_verts_ndarray = np.array([ @@ -383,10 +386,10 @@ def test_verts_calc_area(self): [75, 20, 0], [85, 10, 0]]), ]) # load our vertices into a UXarray Grid object - verts_grid = ux.Grid(faces_verts_ndarray, - vertices=True, - islatlon=True, - concave=False) + verts_grid = ux.open_grid(faces_verts_ndarray, + vertices=True, + islatlon=True, + isconcave=False) face_verts_areas = verts_grid.face_areas @@ -424,16 +427,18 @@ def test_populate_cartesian_xyz_coord(self): ] verts_degree = np.stack((lon_deg, lat_deg), axis=1) - vgrid = ux.Grid([verts_degree], islatlon=False) + + vgrid = ux.open_grid(verts_degree, islatlon=False) vgrid._populate_cartesian_xyz_coord() + for i in range(0, vgrid.nMesh2_node): - nt.assert_almost_equal(vgrid.ds["Mesh2_node_cart_x"].values[i], + nt.assert_almost_equal(vgrid._ds["Mesh2_node_cart_x"].values[i], cart_x[i], decimal=12) - nt.assert_almost_equal(vgrid.ds["Mesh2_node_cart_y"].values[i], + nt.assert_almost_equal(vgrid._ds["Mesh2_node_cart_y"].values[i], cart_y[i], decimal=12) - nt.assert_almost_equal(vgrid.ds["Mesh2_node_cart_z"].values[i], + nt.assert_almost_equal(vgrid._ds["Mesh2_node_cart_z"].values[i], cart_z[i], decimal=12) @@ -463,15 +468,16 @@ def test_populate_lonlat_coord(self): ] verts_cart = np.stack((cart_x, cart_y, cart_z), axis=1) - vgrid = ux.Grid([verts_cart], islatlon=False) + + vgrid = ux.open_grid(verts_cart, islatlon=False) vgrid._populate_lonlat_coord() # The connectivity in `__from_vert__()` will be formed in a reverse order lon_deg, lat_deg = zip(*reversed(list(zip(lon_deg, lat_deg)))) for i in range(0, vgrid.nMesh2_node): - nt.assert_almost_equal(vgrid.ds["Mesh2_node_x"].values[i], + nt.assert_almost_equal(vgrid._ds["Mesh2_node_x"].values[i], lon_deg[i], decimal=12) - nt.assert_almost_equal(vgrid.ds["Mesh2_node_y"].values[i], + nt.assert_almost_equal(vgrid._ds["Mesh2_node_y"].values[i], lat_deg[i], decimal=12) @@ -483,13 +489,9 @@ class TestConnectivity(TestCase): ugrid_filepath_02 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" ugrid_filepath_03 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - xrds_maps = xr.open_dataset(mpas_filepath) - xrds_exodus = xr.open_dataset(exodus_filepath) - xrds_ugrid = xr.open_dataset(ugrid_filepath_01) - - grid_mpas = ux.Grid(xrds_maps) - grid_exodus = ux.Grid(xrds_exodus) - grid_ugrid = ux.Grid(xrds_ugrid) + grid_mpas = ux.open_grid(mpas_filepath) + grid_exodus = ux.open_grid(exodus_filepath) + grid_ugrid = ux.open_grid(ugrid_filepath_01) # used from constructing vertices f0_deg = [[120, -20], [130, -10], [120, 0], [105, 0], [95, -10], [105, -20]] @@ -605,6 +607,22 @@ def _revert_edges_conn_to_face_nodes_conn( return np.array(res_face_nodes_connectivity) + def test_build_nNodes_per_face(self): + """Tests the construction of the ``nNodes_per_face`` variable.""" + + # test on grid constructed from sample datasets + grids = [self.grid_mpas, self.grid_exodus, self.grid_ugrid] + + for grid in grids: + # highest possible dimension dimension for a face + max_dimension = grid.nMaxMesh2_face_nodes + + # face must be at least a triangle + min_dimension = 3 + + assert grid.nNodes_per_face.min() >= min_dimension + assert grid.nNodes_per_face.max() <= max_dimension + def test_build_nNodes_per_face(self): """Tests the construction of the ``nNodes_per_face`` variable.""" @@ -626,7 +644,7 @@ def test_build_nNodes_per_face(self): self.f0_deg, self.f1_deg, self.f2_deg, self.f3_deg, self.f4_deg, self.f5_deg, self.f6_deg ] - grid_from_verts = ux.Grid(verts) + grid_from_verts = ux.open_grid(verts) # number of non-fill-value nodes per face expected_nodes_per_face = np.array([6, 3, 4, 6, 6, 4, 4], dtype=int) @@ -641,8 +659,7 @@ def test_edge_nodes_euler(self): ] for grid_path in grid_paths: - grid_xr = xr.open_dataset(grid_path) - grid_ux = ux.Grid(grid_xr) + grid_ux = ux.open_grid(grid_path) n_face = grid_ux.nMesh2_face n_node = grid_ux.nMesh2_node @@ -656,9 +673,8 @@ def test_build_face_edges_connectivity_mpas(self): with known edge nodes.""" # grid with known edge node connectivity - mpas_grid_xr = xr.open_dataset(self.mpas_filepath) - mpas_grid_ux = ux.Grid(mpas_grid_xr) - edge_nodes_expected = mpas_grid_ux.ds['Mesh2_edge_nodes'].values + mpas_grid_ux = ux.open_grid(self.mpas_filepath) + edge_nodes_expected = mpas_grid_ux._ds['Mesh2_edge_nodes'].values # arrange edge nodes in the same manner as Grid._build_edge_node_connectivity edge_nodes_expected.sort(axis=1) @@ -666,7 +682,7 @@ def test_build_face_edges_connectivity_mpas(self): # construct edge nodes mpas_grid_ux._build_edge_node_connectivity(repopulate=True) - edge_nodes_output = mpas_grid_ux.ds['Mesh2_edge_nodes'].values + edge_nodes_output = mpas_grid_ux._ds['Mesh2_edge_nodes'].values self.assertTrue(np.array_equal(edge_nodes_expected, edge_nodes_output)) @@ -684,14 +700,13 @@ def test_build_face_edges_connectivity(self): self.ugrid_filepath_03 ] for ug_file_name in ug_filename_list: - xr_ds = xr.open_dataset(ug_file_name) - tgrid = ux.Grid(xr_ds) + tgrid = ux.open_grid(ug_file_name) - mesh2_face_nodes = tgrid.ds["Mesh2_face_nodes"] + mesh2_face_nodes = tgrid._ds["Mesh2_face_nodes"] tgrid._build_face_edges_connectivity() - mesh2_face_edges = tgrid.ds.Mesh2_face_edges - mesh2_edge_nodes = tgrid.ds.Mesh2_edge_nodes + mesh2_face_edges = tgrid._ds.Mesh2_face_edges + mesh2_edge_nodes = tgrid._ds.Mesh2_edge_nodes # Assert if the mesh2_face_edges sizes are correct. self.assertEqual(mesh2_face_edges.sizes["nMesh2_face"], @@ -701,12 +716,12 @@ def test_build_face_edges_connectivity(self): # Assert if the mesh2_edge_nodes sizes are correct. # Euler formular for determining the edge numbers: n_face = n_edges - n_nodes + 2 - num_edges = mesh2_face_edges.sizes["nMesh2_face"] + tgrid.ds[ + num_edges = mesh2_face_edges.sizes["nMesh2_face"] + tgrid._ds[ "Mesh2_node_x"].sizes["nMesh2_node"] - 2 size = mesh2_edge_nodes.sizes["nMesh2_edge"] self.assertEqual(mesh2_edge_nodes.sizes["nMesh2_edge"], num_edges) - original_face_nodes_connectivity = tgrid.ds.Mesh2_face_nodes.values + original_face_nodes_connectivity = tgrid._ds.Mesh2_face_nodes.values reverted_mesh2_edge_nodes = self._revert_edges_conn_to_face_nodes_conn( edge_nodes_connectivity=mesh2_edge_nodes.values, @@ -720,14 +735,13 @@ def test_build_face_edges_connectivity(self): original_face_nodes_connectivity[i])) def test_build_face_edges_connectivity_mpas(self): - xr_ds = xr.open_dataset(self.mpas_filepath) - tgrid = ux.Grid(xr_ds) + tgrid = ux.open_grid(self.mpas_filepath) - mesh2_face_nodes = tgrid.ds["Mesh2_face_nodes"] + mesh2_face_nodes = tgrid._ds["Mesh2_face_nodes"] tgrid._build_face_edges_connectivity() - mesh2_face_edges = tgrid.ds.Mesh2_face_edges - mesh2_edge_nodes = tgrid.ds.Mesh2_edge_nodes + mesh2_face_edges = tgrid._ds.Mesh2_face_edges + mesh2_edge_nodes = tgrid._ds.Mesh2_edge_nodes # Assert if the mesh2_face_edges sizes are correct. self.assertEqual(mesh2_face_edges.sizes["nMesh2_face"], @@ -737,7 +751,7 @@ def test_build_face_edges_connectivity_mpas(self): # Assert if the mesh2_edge_nodes sizes are correct. # Euler formular for determining the edge numbers: n_face = n_edges - n_nodes + 2 - num_edges = mesh2_face_edges.sizes["nMesh2_face"] + tgrid.ds[ + num_edges = mesh2_face_edges.sizes["nMesh2_face"] + tgrid._ds[ "Mesh2_node_x"].sizes["nMesh2_node"] - 2 size = mesh2_edge_nodes.sizes["nMesh2_edge"] self.assertEqual(mesh2_edge_nodes.sizes["nMesh2_edge"], num_edges) @@ -747,27 +761,27 @@ def test_build_face_edges_connectivity_fillvalues(self): self.f0_deg, self.f1_deg, self.f2_deg, self.f3_deg, self.f4_deg, self.f5_deg, self.f6_deg ] - uds = ux.Grid(verts) + uds = ux.open_grid(verts) uds._build_face_edges_connectivity() - n_face = len(uds.ds["Mesh2_face_edges"].values) + n_face = len(uds._ds["Mesh2_face_edges"].values) n_node = uds.nMesh2_node - n_edge = len(uds.ds["Mesh2_edge_nodes"].values) + n_edge = len(uds._ds["Mesh2_edge_nodes"].values) self.assertEqual(7, n_face) self.assertEqual(21, n_node) self.assertEqual(28, n_edge) # We will utilize the edge_nodes_connectivity and face_edges_connectivity to generate the - # res_face_nodes_connectivity and compare it with the uds.ds["Mesh2_face_nodes"].values - edge_nodes_connectivity = uds.ds["Mesh2_edge_nodes"].values - face_edges_connectivity = uds.ds["Mesh2_face_edges"].values - face_nodes_connectivity = uds.ds["Mesh2_face_nodes"].values + # res_face_nodes_connectivity and compare it with the uds._ds["Mesh2_face_nodes"].values + edge_nodes_connectivity = uds._ds["Mesh2_edge_nodes"].values + face_edges_connectivity = uds._ds["Mesh2_face_edges"].values + face_nodes_connectivity = uds._ds["Mesh2_face_nodes"].values res_face_nodes_connectivity = self._revert_edges_conn_to_face_nodes_conn( edge_nodes_connectivity, face_edges_connectivity, face_nodes_connectivity) - # Compare the res_face_nodes_connectivity with the uds.ds["Mesh2_face_nodes"].values + # Compare the res_face_nodes_connectivity with the uds._ds["Mesh2_face_nodes"].values self.assertTrue( np.array_equal(res_face_nodes_connectivity, - uds.ds["Mesh2_face_nodes"].values)) + uds._ds["Mesh2_face_nodes"].values)) diff --git a/test/test_helpers.py b/test/test_helpers.py index fd6a748cd..fe2caee30 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -9,8 +9,8 @@ import uxarray as ux -from uxarray.helpers import _replace_fill_values -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.helpers import _replace_fill_values +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE try: import constants @@ -20,8 +20,9 @@ # Data files current_path = Path(os.path.dirname(os.path.realpath(__file__))) -exodus = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" -ne8 = current_path / 'meshfiles' / "scrip" / "outCSne8" / 'outCSne8.nc' +gridfile_exo_CSne8 = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" +gridfile_scrip_CSne8 = current_path / 'meshfiles' / "scrip" / "outCSne8" / 'outCSne8.nc' + err_tolerance = 1.0e-12 @@ -33,8 +34,10 @@ def test_face_area_coords(self): x = np.array([0.57735027, 0.57735027, -0.57735027]) y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) z = np.array([-0.57735027, -0.57735027, -0.57735027]) + face_nodes = np.array([[0, 1, 2]]).astype(INT_DTYPE) face_dimension = np.array([3], dtype=INT_DTYPE) + area = ux.get_all_face_area_from_coords(x, y, z, @@ -42,6 +45,7 @@ def test_face_area_coords(self): face_dimension, 3, coords_type="cartesian") + nt.assert_almost_equal(area, constants.TRI_AREA, decimal=1) def test_calculate_face_area(self): @@ -51,7 +55,9 @@ def test_calculate_face_area(self): x = np.array([0.57735027, 0.57735027, -0.57735027]) y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) z = np.array([-0.57735027, -0.57735027, -0.57735027]) + area = ux.calculate_face_area(x, y, z, "gaussian", 5, "cartesian") + nt.assert_almost_equal(area, constants.TRI_AREA, decimal=3) def test_quadrature(self): @@ -64,6 +70,7 @@ def test_quadrature(self): np.testing.assert_array_almost_equal(W, dW) dG, dW = ux.get_gauss_quadratureDG(order) + G = np.array([[0.5]]) W = np.array([1.0]) @@ -76,14 +83,14 @@ class TestGridCenter(TestCase): def test_grid_center(self): """Calculates if the calculated center point of a grid box is the same as a given value for the same dataset.""" - ds_ne8 = xr.open_dataset(ne8) + ds_scrip_CSne8 = xr.open_dataset(gridfile_scrip_CSne8) # select actual center_lat/lon - scrip_center_lon = ds_ne8['grid_center_lon'] - scrip_center_lat = ds_ne8['grid_center_lat'] + scrip_center_lon = ds_scrip_CSne8['grid_center_lon'] + scrip_center_lat = ds_scrip_CSne8['grid_center_lat'] # Calculate the center_lat/lon using same dataset's corner_lat/lon - calc_center = ux.grid_center_lat_lon(ds_ne8) + calc_center = ux.grid_center_lat_lon(ds_scrip_CSne8) calc_lat = calc_center[0] calc_lon = calc_center[1] @@ -95,20 +102,24 @@ def test_grid_center(self): class TestCoordinatesConversion(TestCase): def test_normalize_in_place(self): - [x, y, z] = ux.helpers.normalize_in_place( + [x, y, z] = ux.utils.helpers.normalize_in_place( [random.random(), random.random(), random.random()]) + self.assertLessEqual(np.absolute(np.sqrt(x * x + y * y + z * z) - 1), err_tolerance) def test_node_xyz_to_lonlat_rad(self): - [x, y, z] = ux.helpers.normalize_in_place([ + [x, y, z] = ux.utils.helpers.normalize_in_place([ random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-1, 1) ]) - [lon, lat] = ux.helpers.node_xyz_to_lonlat_rad([x, y, z]) - [new_x, new_y, new_z] = ux.helpers.node_lonlat_rad_to_xyz([lon, lat]) + + [lon, lat] = ux.utils.helpers.node_xyz_to_lonlat_rad([x, y, z]) + [new_x, new_y, + new_z] = ux.utils.helpers.node_lonlat_rad_to_xyz([lon, lat]) + self.assertLessEqual(np.absolute(new_x - x), err_tolerance) self.assertLessEqual(np.absolute(new_y - y), err_tolerance) self.assertLessEqual(np.absolute(new_z - z), err_tolerance) @@ -118,8 +129,11 @@ def test_node_latlon_rad_to_xyz(self): random.uniform(0, 2 * np.pi), random.uniform(-0.5 * np.pi, 0.5 * np.pi) ] - [x, y, z] = ux.helpers.node_lonlat_rad_to_xyz([lon, lat]) - [new_lon, new_lat] = ux.helpers.node_xyz_to_lonlat_rad([x, y, z]) + + [x, y, z] = ux.utils.helpers.node_lonlat_rad_to_xyz([lon, lat]) + + [new_lon, new_lat] = ux.utils.helpers.node_xyz_to_lonlat_rad([x, y, z]) + self.assertLessEqual(np.absolute(new_lon - lon), err_tolerance) self.assertLessEqual(np.absolute(new_lat - lat), err_tolerance) diff --git a/test/test_mpas.py b/test/test_mpas.py index 58dda89d8..d95dd3632 100644 --- a/test/test_mpas.py +++ b/test/test_mpas.py @@ -1,5 +1,5 @@ -from uxarray._mpas import _replace_padding, _replace_zeros, _to_zero_index -from uxarray._mpas import _read_mpas, _primal_to_ugrid, _dual_to_ugrid +from uxarray.io._mpas import _replace_padding, _replace_zeros, _to_zero_index +from uxarray.io._mpas import _read_mpas import uxarray as ux import xarray as xr from unittest import TestCase @@ -7,7 +7,7 @@ import os from pathlib import Path -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE current_path = Path(os.path.dirname(os.path.realpath(__file__))) @@ -29,8 +29,8 @@ def test_read_mpas(self): def test_mpas_to_grid(self): """Tests creation of Grid object from converted MPAS dataset.""" - mpas_uxgrid_primal = ux.Grid(self.mpas_xr_ds, use_dual=False) - mpas_uxgrid_primal = ux.Grid(self.mpas_xr_ds, use_dual=True) + mpas_uxgrid_primal = ux.open_grid(self.mpas_grid_path, use_dual=False) + mpas_uxgrid_primal = ux.open_grid(self.mpas_grid_path, use_dual=True) def test_primal_to_ugrid_conversion(self): """Verifies that the Primal-Mesh was converted properly.""" @@ -100,8 +100,8 @@ def test_add_fill_values(self): assert np.array_equal(verticesOnCell, gold_output) def test_set_attrs(self): - """Tests the execution of "_set_global_attrs", checking for attributes - being correctly stored in "Grid.ds".""" + """Tests the execution of ``_set_global_attrs``, checking for + attributes being correctly stored in ``Grid._ds``""" # full set of expected mpas attributes expected_attrs = [ @@ -123,4 +123,4 @@ def test_set_attrs(self): # check if all expected attributes are set for mpas_attr in expected_attrs: - assert mpas_attr in uxgrid.ds.attrs + assert mpas_attr in uxgrid._ds.attrs diff --git a/test/test_scrip.py b/test/test_scrip.py index 34da85a3f..b9fd79ac1 100644 --- a/test/test_scrip.py +++ b/test/test_scrip.py @@ -1,21 +1,23 @@ -from uxarray._scrip import _read_scrip, _encode_scrip -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE import uxarray as ux import xarray as xr from unittest import TestCase import numpy as np +import numpy.testing as nt import os from pathlib import Path +from uxarray.io._scrip import _read_scrip, _encode_scrip +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE + current_path = Path(os.path.dirname(os.path.realpath(__file__))) -ne30 = current_path / 'meshfiles' / "ugrid" / "outCSne30" / 'outCSne30.ug' -ne8 = current_path / 'meshfiles' / "scrip" / "outCSne8" / 'outCSne8.nc' +gridfile_ne30 = current_path / 'meshfiles' / "ugrid" / "outCSne30" / 'outCSne30.ug' +gridfile_ne8 = current_path / 'meshfiles' / "scrip" / "outCSne8" / 'outCSne8.nc' -ds_ne30 = xr.open_dataset(ne30, decode_times=False, +ds_ne30 = xr.open_dataset(gridfile_ne30, decode_times=False, engine='netcdf4') # mesh2_node_x/y -ds_ne8 = xr.open_dataset(ne8, decode_times=False, +ds_ne8 = xr.open_dataset(gridfile_ne8, decode_times=False, engine='netcdf4') # grid_corner_lat/lon @@ -51,8 +53,7 @@ def test_encode_scrip(self): scrip_in_ds.to_netcdf(str(new_path)) # Save as new file # Use xarray open_dataset, create a uxarray grid object to then create SCRIP file from new UGRID file - xr_obj = xr.open_dataset(str(new_path)) - ugrid_out = ux.Grid(xr_obj) + ugrid_out = ux.open_grid(new_path) scrip_encode_ds = _encode_scrip(ugrid_out.Mesh2_face_nodes, ugrid_out.Mesh2_node_x, @@ -60,24 +61,24 @@ def test_encode_scrip(self): ugrid_out.face_areas) # Test newly created SCRIP is same as original SCRIP - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_corner_lat'], - ds_ne8['grid_corner_lat']) - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_corner_lon'], - ds_ne8['grid_corner_lon']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_corner_lat'], + ds_ne8['grid_corner_lat']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_corner_lon'], + ds_ne8['grid_corner_lon']) # Tests that calculated center lat/lon values are equivalent to original - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_center_lon'], - ds_ne8['grid_center_lon']) - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_center_lat'], - ds_ne8['grid_center_lat']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_center_lon'], + ds_ne8['grid_center_lon']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_center_lat'], + ds_ne8['grid_center_lat']) # Tests that calculated face area values are equivalent to original - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_area'], - ds_ne8['grid_area']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_area'], + ds_ne8['grid_area']) # Tests that calculated grid imask values are equivalent to original - np.testing.assert_array_almost_equal(scrip_encode_ds['grid_imask'], - ds_ne8['grid_imask']) + nt.assert_array_almost_equal(scrip_encode_ds['grid_imask'], + ds_ne8['grid_imask']) # Test that "mesh" variables are not in new file with self.assertRaises(KeyError): @@ -87,10 +88,10 @@ def test_encode_scrip(self): def test_scrip_variable_names(self): """Tests that returned dataset from writer function has all required SCRIP variables.""" - xr_ne30 = xr.open_dataset(ne30) - ux_ne30 = ux.Grid(xr_ne30) - scrip30 = _encode_scrip(ux_ne30.Mesh2_face_nodes, ux_ne30.Mesh2_node_x, - ux_ne30.Mesh2_node_y, ux_ne30.face_areas) + uxds_ne30 = ux.open_grid(gridfile_ne30) + scrip30 = _encode_scrip(uxds_ne30.Mesh2_face_nodes, + uxds_ne30.Mesh2_node_x, uxds_ne30.Mesh2_node_y, + uxds_ne30.face_areas) # List of relevant variable names for a scrip file var_list = [ @@ -116,11 +117,8 @@ def test_standardized_dtype_and_fill(self): """Test to see if Mesh2_Face_Nodes uses the expected integer datatype and expected fill value as set in constants.py.""" - xr_ne30 = xr.open_dataset(ne30) - ux_grid_01 = ux.Grid(xr_ne30) - - xr_ne8 = xr.open_dataset(ne8) - ux_grid_02 = ux.Grid(xr_ne8) + ux_grid_01 = ux.open_grid(gridfile_ne30) + ux_grid_02 = ux.open_grid(gridfile_ne8) grids = [ux_grid_01, ux_grid_02] for grid in grids: diff --git a/test/test_ugrid.py b/test/test_ugrid.py index 84cf6d9f8..c1c9b660c 100644 --- a/test/test_ugrid.py +++ b/test/test_ugrid.py @@ -4,9 +4,10 @@ from unittest import TestCase from pathlib import Path import warnings +import numpy.testing as nt import uxarray as ux -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE try: import constants @@ -15,28 +16,28 @@ current_path = Path(os.path.dirname(os.path.realpath(__file__))) +gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" +gridfile_RLL10deg_ne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" -class TestUgrid(TestCase): +gridfile_exo_ne8 = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - def test_read_ugrid(self): - """Reads a ugrid file.""" - ug_filename1 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" +class TestUgrid(TestCase): - xr_grid1 = xr.open_dataset(str(ug_filename1)) - xr_grid2 = xr.open_dataset(str(ug_filename2)) - xr_grid3 = xr.open_dataset(str(ug_filename3)) + def test_read_ugrid(self): + """Reads a ugrid file."""\ - ux_grid1 = ux.Grid(xr_grid1) - ux_grid2 = ux.Grid(xr_grid2) - ux_grid3 = ux.Grid(xr_grid3) + uxgrid_ne30 = ux.open_grid(str(gridfile_ne30)) + uxgrid_RLL1deg = ux.open_grid(str(gridfile_RLL1deg)) + uxgrid_RLL10deg_ne4 = ux.open_grid(str(gridfile_RLL10deg_ne4)) - assert (ux_grid1.Mesh2_node_x.size == constants.NNODES_outCSne30) - assert (ux_grid2.Mesh2_node_x.size == constants.NNODES_outRLL1deg) - assert ( - ux_grid3.Mesh2_node_x.size == constants.NNODES_ov_RLL10deg_CSne4) + nt.assert_equal(uxgrid_ne30.Mesh2_node_x.size, + constants.NNODES_outCSne30) + nt.assert_equal(uxgrid_RLL1deg.Mesh2_node_x.size, + constants.NNODES_outRLL1deg) + nt.assert_equal(uxgrid_RLL10deg_ne4.Mesh2_node_x.size, + constants.NNODES_ov_RLL10deg_CSne4) def test_read_ugrid_opendap(self): """Read an ugrid model from an OPeNDAP URL.""" @@ -44,7 +45,7 @@ def test_read_ugrid_opendap(self): try: # make sure we can read the ugrid file from the OPeNDAP URL url = "http://test.opendap.org:8080/opendap/ugrid/NECOFS_GOM3_FORECAST.nc" - xr_grid = xr.open_dataset(url, drop_variables="siglay") + uxgrid_url = ux.open_grid(url, drop_variables="siglay") except OSError: # print warning and pass if we can't connect to the OPeNDAP server @@ -52,17 +53,16 @@ def test_read_ugrid_opendap(self): pass else: - ugrid = ux.Grid(xr_grid) - assert isinstance(getattr(ugrid, "Mesh2_node_x"), xr.DataArray) - assert isinstance(getattr(ugrid, "Mesh2_node_y"), xr.DataArray) - assert isinstance(getattr(ugrid, "Mesh2_face_nodes"), xr.DataArray) + + assert isinstance(getattr(uxgrid_url, "Mesh2_node_x"), xr.DataArray) + assert isinstance(getattr(uxgrid_url, "Mesh2_node_y"), xr.DataArray) + assert isinstance(getattr(uxgrid_url, "Mesh2_face_nodes"), + xr.DataArray) def test_encode_ugrid(self): """Read an Exodus dataset and encode that as a UGRID format.""" - exo2_filename = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - xr_grid = xr.open_dataset(str(exo2_filename)) - ux_grid = ux.Grid(xr_grid) + ux_grid = ux.open_grid(gridfile_exo_ne8) ux_grid.encode_as("ugrid") def test_standardized_dtype_and_fill(self): @@ -73,13 +73,9 @@ def test_standardized_dtype_and_fill(self): ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - xr_grid1 = xr.open_dataset(str(ug_filename1)) - xr_grid2 = xr.open_dataset(str(ug_filename2)) - xr_grid3 = xr.open_dataset(str(ug_filename3)) - - ux_grid1 = ux.Grid(xr_grid1) - ux_grid2 = ux.Grid(xr_grid2) - ux_grid3 = ux.Grid(xr_grid3) + ux_grid1 = ux.open_grid(ug_filename1) + ux_grid2 = ux.open_grid(ug_filename2) + ux_grid3 = ux.open_grid(ug_filename3) # check for correct dtype and fill value grids_with_fill = [ux_grid2] @@ -100,8 +96,8 @@ def test_standardized_dtype_and_fill_dask(self): with dask chunking """ ug_filename = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - xr_grid = xr.open_dataset(str(ug_filename), chunks={'nMesh2_node': 100}) - ux_grid = ux.Grid(xr_grid) + ux_grid = ux.open_grid(ug_filename) + assert ux_grid.Mesh2_face_nodes.dtype == INT_DTYPE assert ux_grid.Mesh2_face_nodes._FillValue == INT_FILL_VALUE assert INT_FILL_VALUE in ux_grid.Mesh2_face_nodes.values diff --git a/uxarray/__init__.py b/uxarray/__init__.py index a1cfa76e0..b9aedd249 100644 --- a/uxarray/__init__.py +++ b/uxarray/__init__.py @@ -1,5 +1,10 @@ -from .grid import * -from .helpers import * +from uxarray.utils.helpers import * +from uxarray.utils.constants import (INT_DTYPE, INT_FILL_VALUE) +from uxarray.core.grid import Grid +from uxarray.core.api import (open_grid, open_dataset, open_mfdataset) +from uxarray.core.dataarray import UxDataArray +from uxarray.core.dataset import UxDataset + # Sets the version of uxarray currently installed # Attempt to import the needed modules try: diff --git a/uxarray/core/__init__.py b/uxarray/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/uxarray/core/api.py b/uxarray/core/api.py new file mode 100644 index 000000000..a17f4ad3e --- /dev/null +++ b/uxarray/core/api.py @@ -0,0 +1,263 @@ +"""UXarray dataset module.""" + +import os + +from pathlib import Path +from typing import Any, Dict, Optional, Union + +import numpy as np +import xarray as xr + +from uxarray.core.grid import Grid + +from uxarray.core.dataset import UxDataset + + +def open_grid(grid_filename_or_obj: Union[str, Path, xr.DataArray, np.ndarray, + list, tuple], + gridspec: Optional[str] = None, + vertices: Optional[list] = None, + islatlon: Optional[bool] = False, + isconcave: Optional[bool] = False, + use_dual: Optional[bool] = False, + **kwargs: Dict[str, Any]) -> Grid: + """Creates a ``uxarray.Grid`` object from a grid topology definition. + + Parameters + ---------- + + grid_filename_or_obj : string, xarray.Dataset, ndarray, list, tuple, required + String or Path object as a path to a netCDF file or an OpenDAP URL that + stores the unstructured grid topology/definition. It is read similar to + ``filename_or_obj`` in ``xarray.open_dataset``. Otherwise, either + ``xr.DataArray``, ``np.ndarray``, ``list``, or ``tuple`` as a vertices + object to define the grid. + + islatlon : bool, optional + Specify if the grid is lat/lon based + + isconcave: bool, optional + Specify if this grid has concave elements (internal checks for this are possible) + + gridspec: str, optional + Specifies gridspec + + vertices: bool, optional + Whether to create grid from vertices + + source_grid: str, optional + Path or URL to the source grid file. For diagnostic/reporting purposes only. + + use_dual: bool, optional + Specify whether to use the primal (use_dual=False) or dual (use_dual=True) mesh if the file type is mpas + + **kwargs : Dict[str, Any] + Additional arguments passed on to ``xarray.open_dataset``. Refer to the + [xarray + docs](https://xarray.pydata.org/en/stable/generated/xarray.open_dataset.html) + for accepted keyword arguments. + + Returns + ------- + + uxgrid : uxarray.Grid + Initialized Grid Object from Input Grid File + + Examples + -------- + + Open dataset with a grid topology file + + >>> import uxarray as ux + >>> uxgrid = ux.open_grid("grid_filename.g") + """ + + # Grid definition + if isinstance(grid_filename_or_obj, + (list, tuple, np.ndarray, xr.DataArray)): + uxgrid = Grid(grid_filename_or_obj, + gridspec=gridspec, + vertices=vertices, + islatlon=islatlon, + isconcave=isconcave, + source_grid=str(grid_filename_or_obj), + use_dual=use_dual) + else: + grid_ds = xr.open_dataset(grid_filename_or_obj, + decode_times=False, + **kwargs) # type: ignore + + uxgrid = Grid(grid_ds, + gridspec=gridspec, + vertices=vertices, + islatlon=islatlon, + isconcave=isconcave, + source_grid=str(grid_filename_or_obj), + use_dual=use_dual) + + return uxgrid + + +def open_dataset(grid_filename_or_obj: str, + filename_or_obj: str, + gridspec: Optional[str] = None, + vertices: Optional[list] = None, + islatlon: Optional[bool] = False, + isconcave: Optional[bool] = False, + use_dual: Optional[bool] = False, + **kwargs: Dict[str, Any]) -> UxDataset: + """Wraps ``xarray.open_dataset()``, given a grid topology definition with a + single dataset file or object with corresponding data. + + Parameters + ---------- + + grid_filename_or_obj : string, required + String or Path object as a path to a netCDF file or an OpenDAP URL that + stores the unstructured grid definition that the dataset belongs to. It + is read similar to ``filename_or_obj`` in ``xarray.open_dataset``. + + filename_or_obj : string, required + String or Path object as a path to a netCDF file or an OpenDAP URL that + stores the actual data set. It is the same ``filename_or_obj`` in + ``xarray.open_dataset``. + + islatlon : bool, optional + Specify if the grid is lat/lon based + + isconcave: bool, optional + Specify if this grid has concave elements (internal checks for this are possible) + + gridspec: str, optional + Specifies gridspec + + vertices: bool, optional + Whether to create grid from vertices + + source_grid: str, optional + Path or URL to the source grid file. For diagnostic/reporting purposes only. + + use_dual: bool, optional + Specify whether to use the primal (use_dual=False) or dual (use_dual=True) mesh if the file type is mpas + + **kwargs : Dict[str, Any] + Additional arguments passed on to ``xarray.open_dataset``. Refer to the + [xarray + docs](https://xarray.pydata.org/en/stable/generated/xarray.open_dataset.html) + for accepted keyword arguments. + + Returns + ------- + + uxds : uxarray.UxDataset + Dataset with linked `uxgrid` property of type `Grid`. + + Examples + -------- + + Open dataset with a grid topology file + + >>> import uxarray as ux + >>> ux_ds = ux.open_dataset("grid_filename.g", "grid_filename_vortex.nc") + """ + + ## Grid definition + uxgrid = open_grid(grid_filename_or_obj, + gridspec=gridspec, + vertices=vertices, + islatlon=islatlon, + isconcave=isconcave, + use_dual=use_dual) + + ## UxDataset + ds = xr.open_dataset(filename_or_obj, decode_times=False, + **kwargs) # type: ignore + + uxds = UxDataset(ds, uxgrid=uxgrid, source_datasets=str(filename_or_obj)) + + return uxds + + +def open_mfdataset(grid_filename_or_obj: str, + paths: Union[str, os.PathLike], + gridspec: Optional[str] = None, + vertices: Optional[list] = None, + islatlon: Optional[bool] = False, + isconcave: Optional[bool] = False, + use_dual: Optional[bool] = False, + **kwargs: Dict[str, Any]) -> UxDataset: + """Wraps ``xarray.open_mfdataset()``, given a single grid topology file + with multiple dataset paths with corresponding data. + + Parameters + ---------- + + grid_filename_or_obj : string, required + String or Path object as a path to a netCDF file or an OpenDAP URL that + stores the unstructured grid definition that the dataset belongs to. It + is read similar to ``filename_or_obj`` in ``xarray.open_dataset``. + + paths : string, required + Either a string glob in the form "path/to/my/files/*.nc" or an explicit + list of files to open. It is the same ``paths`` in ``xarray.open_mfdataset``. + + islatlon : bool, optional + Specify if the grid is lat/lon based + + isconcave: bool, optional + Specify if this grid has concave elements (internal checks for this are possible) + + gridspec: str, optional + Specifies gridspec + + vertices: bool, optional + Whether to create grid from vertices + + source_grid: str, optional + Path or URL to the source grid file. For diagnostic/reporting purposes only. + + use_dual: bool, optional + Specify whether to use the primal (use_dual=False) or dual (use_dual=True) mesh if the file type is mpas + + **kwargs : Dict[str, Any] + Additional arguments passed on to ``xarray.open_mfdataset``. Refer to the + [xarray + docs](https://xarray.pydata.org/en/stable/generated/xarray.open_mfdataset.html) + for accepted keyword arguments. + + Returns + ------- + + object : uxarray.UxDataset + Dataset with the unstructured grid. + + Examples + -------- + + Open grid file along with multiple data files (two or more) + + >>> import uxarray as ux + + 1. Open from an explicit list of dataset files + + >>> ux_ds = ux.open_mfdataset("grid_filename.g", "grid_filename_vortex_1.nc", "grid_filename_vortex_2.nc") + + 2. Open from a string glob + + >>> ux_ds = ux.open_mfdataset("grid_filename.g", "grid_filename_vortex_*.nc") + """ + + ## Grid definition + uxgrid = open_grid(grid_filename_or_obj, + gridspec=gridspec, + vertices=vertices, + islatlon=islatlon, + isconcave=isconcave, + use_dual=use_dual) + + ## UxDataset + ds = xr.open_mfdataset(paths, decode_times=False, **kwargs) # type: ignore + + uxds = UxDataset(ds, uxgrid=uxgrid, source_datasets=str(paths)) + + return uxds diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py new file mode 100644 index 000000000..72f46102b --- /dev/null +++ b/uxarray/core/dataarray.py @@ -0,0 +1,99 @@ +import numpy as np +import xarray as xr + +from typing import Optional + +from uxarray.core.grid import Grid + + +class UxDataArray(xr.DataArray): + """N-dimensional ``xarray.DataArray``-like array. Inherits from + ``xarray.DataArray`` and has its own unstructured grid-aware array + operators and attributes through the ``uxgrid`` accessor. + + Parameters + ---------- + uxgrid : uxarray.Grid, optional + The `Grid` object that makes this array aware of the unstructured + grid topology it belongs to. + + If `None`, it needs to be an instance of `uxarray.Grid`. + + Other Parameters + ---------------- + *args: + Arguments for the ``xarray.DataArray`` class + **kwargs: + Keyword arguments for the ``xarray.DataArray`` class + + Notes + ----- + See `xarray.DataArray `__ + for further information about DataArrays. + """ + + # expected instance attributes, required for subclassing with xarray (as of v0.13.0) + __slots__ = ("_uxgrid",) + + def __init__(self, *args, uxgrid: Grid = None, **kwargs): + + self._uxgrid = None + + if uxgrid is not None and not isinstance(uxgrid, Grid): + raise RuntimeError( + "uxarray.UxDataArray.__init__: uxgrid can be either None or " + "an instance of the uxarray.Grid class") + else: + self.uxgrid = uxgrid + + super().__init__(*args, **kwargs) + + @classmethod + def _construct_direct(cls, *args, **kwargs): + """Override to make the result a ``uxarray.UxDataArray`` class.""" + return cls(xr.DataArray._construct_direct(*args, **kwargs)) + + def _copy(self, **kwargs): + """Override to make the result a complete instance of + ``uxarray.UxDataArray``.""" + copied = super()._copy(**kwargs) + + deep = kwargs.get('deep', None) + + if deep == True: + # Reinitialize the uxgrid assessor + copied.uxgrid = self.uxgrid.copy() # deep copy + else: + # Point to the existing uxgrid object + copied.uxgrid = self.uxgrid + + return copied + + def _replace(self, *args, **kwargs): + """Override to make the result a complete instance of + ``uxarray.UxDataArray``.""" + da = super()._replace(*args, **kwargs) + + if isinstance(da, UxDataArray): + da.uxgrid = self.uxgrid + else: + da = UxDataArray(da, uxgrid=self.uxgrid) + + return da + + @property + def uxgrid(self): + """``uxarray.Grid`` property for ``uxarray.UxDataArray`` to make it + unstructured grid-aware. + + Examples + -------- + uxds = ux.open_dataset(grid_path, data_path) + uxds..uxgrid + """ + return self._uxgrid + + # a setter function + @uxgrid.setter + def uxgrid(self, ugrid_obj): + self._uxgrid = ugrid_obj diff --git a/uxarray/core/dataset.py b/uxarray/core/dataset.py new file mode 100644 index 000000000..fdb1097ed --- /dev/null +++ b/uxarray/core/dataset.py @@ -0,0 +1,307 @@ +import numpy as np +import xarray as xr + +import sys + +from collections.abc import Hashable + +from typing import Optional, IO + +from uxarray.core.dataarray import UxDataArray +from uxarray.core.grid import Grid + + +class UxDataset(xr.Dataset): + """A ``xarray.Dataset``-like, multi-dimensional, in memory, array database. + Inherits from ``xarray.Dataset`` and has its own unstructured grid-aware + dataset operators and attributes through the ``uxgrid`` accessor. + + Parameters + ---------- + uxgrid : uxarray.Grid, optional + The ``Grid`` object that makes this array aware of the unstructured + grid topology it belongs to. + + If ``None``, it needs to be an instance of ``uxarray.Grid``. + + Other Parameters + ---------------- + *args: + Arguments for the ``xarray.Dataset`` class + **kwargs: + Keyword arguments for the ``xarray.Dataset`` class + + Notes + ----- + See `xarray.Dataset `__ + for further information about Datasets. + """ + + # expected instance attributes, required for subclassing with xarray (as of v0.13.0) + __slots__ = ( + '_uxgrid', + '_source_datasets', + ) + + def __init__(self, + *args, + uxgrid: Grid = None, + source_datasets: Optional[str] = None, + **kwargs): + + self._uxgrid = None + self._source_datasets = source_datasets + # setattr(self, 'source_datasets', source_datasets) + + if uxgrid is not None and not isinstance(uxgrid, Grid): + raise RuntimeError( + "uxarray.UxDataset.__init__: uxgrid can be either None or " + "an instance of the `uxarray.Grid` class") + else: + self.uxgrid = uxgrid + + super().__init__(*args, **kwargs) + + def __getitem__(self, key): + """Override to make sure the result is an instance of + ``uxarray.UxDataArray`` or ``uxarray.UxDataset``.""" + + value = super().__getitem__(key) + + if isinstance(value, xr.DataArray): + value = UxDataArray(value, uxgrid=self.uxgrid) + elif isinstance(value, xr.Dataset): + value = UxDataset(value, + uxgrid=self.uxgrid, + source_datasets=self.source_datasets) + + return value + + # def __setitem__(self, key, value): + # """Override to make sure the `value` is an instance of + # ``uxarray.UxDataArray``.""" + # if isinstance(value, xr.DataArray): + # value = UxDataArray(value, uxgrid=self.uxgrid) + # + # if isinstance(value, UxDataArray): + # value = value.to_dataarray() + # + # super().__setitem__(key, value) + + @property + def source_datasets(self): + """Property to keep track of the source data sets used to instantiate + this ``uxarray.UxDataset``. + + Can be used as metadata for diagnosis purposes. + + Examples + -------- + uxds = ux.open_dataset(grid_path, data_path) + uxds.source_datasets + """ + return self._source_datasets + + # a setter function + @source_datasets.setter + def source_datasets(self, source_datasets_input): + self._source_datasets = source_datasets_input + + @property + def uxgrid(self): + """``uxarray.Grid`` property for ``uxarray.UxDataset`` to make it + unstructured grid-aware. + + Examples + -------- + uxds = ux.open_dataset(grid_path, data_path) + uxds.uxgrid + """ + return self._uxgrid + + # a setter function + @uxgrid.setter + def uxgrid(self, ugrid_obj): + self._uxgrid = ugrid_obj + + def _calculate_binary_op(self, *args, **kwargs): + """Override to make the result a complete instance of + ``uxarray.UxDataset``.""" + ds = super()._calculate_binary_op(*args, **kwargs) + + if isinstance(ds, UxDataset): + ds.uxgrid = self.uxgrid + ds.source_datasets = self.source_datasets + else: + ds = UxDataset(ds, + uxgrid=self.uxgrid, + source_datasets=self.source_datasets) + + return ds + + def _construct_dataarray(self, name) -> UxDataArray: + """Override to make the result an instance of + ``uxarray.UxDataArray``.""" + xarr = super()._construct_dataarray(name) + return UxDataArray(xarr, uxgrid=self.uxgrid) + + @classmethod + def _construct_direct(cls, *args, **kwargs): + """Override to make the result an ``uxarray.UxDataset`` class.""" + + return cls(xr.Dataset._construct_direct(*args, **kwargs)) + + def _copy(self, **kwargs): + """Override to make the result a complete instance of + ``uxarray.UxDataset``.""" + copied = super()._copy(**kwargs) + + deep = kwargs.get('deep', None) + + if deep == True: + # Reinitialize the uxgrid assessor + copied.uxgrid = self.uxgrid.copy() # deep copy + else: + # Point to the existing uxgrid object + copied.uxgrid = self.uxgrid + + return copied + + def _replace(self, *args, **kwargs): + """Override to make the result a complete instance of + ``uxarray.UxDataset``.""" + ds = super()._replace(*args, **kwargs) + + if isinstance(ds, UxDataset): + ds.uxgrid = self.uxgrid + ds.source_datasets = self.source_datasets + else: + ds = UxDataset(ds, + uxgrid=self.uxgrid, + source_datasets=self.source_datasets) + + return ds + + @classmethod + def from_dataframe(cls, dataframe): + """Override to make the result a ``uxarray.UxDataset`` class.""" + + return cls( + { + col: ('index', dataframe[col].values) + for col in dataframe.columns + }, + coords={'index': dataframe.index}) + + @classmethod + def from_dict(cls, data, **kwargs): + """Override to make the result a ``uxarray.UxDataset`` class.""" + + return cls({key: ('index', val) for key, val in data.items()}, + coords={'index': range(len(next(iter(data.values()))))}, + **kwargs) + + def info(self, buf: IO = None, show_attrs=False) -> None: + """Concise summary of Dataset variables and attributes including grid + topology information stored in the ``uxgrid`` property. + + Parameters + ---------- + buf : file-like, default: sys.stdout + writable buffer + show_attrs : bool + Flag to select whether to show attributes + + See Also + -------- + pandas.DataFrame.assign + ncdump : netCDF's ncdump + """ + if buf is None: # pragma: no cover + buf = sys.stdout + + lines = [] + lines.append("uxarray.Dataset {") + + lines.append("grid topology dimensions:") + for name, size in self.uxgrid._ds.dims.items(): + lines.append(f"\t{name} = {size}") + + lines.append("\ngrid topology variables:") + for name, da in self.uxgrid._ds.variables.items(): + dims = ", ".join(map(str, da.dims)) + lines.append(f"\t{da.dtype} {name}({dims})") + if show_attrs: + for k, v in da.attrs.items(): + lines.append(f"\t\t{name}:{k} = {v}") + + lines.append("\ndata dimensions:") + for name, size in self.dims.items(): + lines.append(f"\t{name} = {size}") + + lines.append("\ndata variables:") + for name, da in self.variables.items(): + dims = ", ".join(map(str, da.dims)) + lines.append(f"\t{da.dtype} {name}({dims})") + if show_attrs: + for k, v in da.attrs.items(): + lines.append(f"\t\t{name}:{k} = {v}") + + if show_attrs: + lines.append("\nglobal attributes:") + for k, v in self.attrs.items(): + lines.append(f"\t:{k} = {v}") + + lines.append("}") + buf.write("\n".join(lines)) + + def integrate(self, quadrature_rule="triangular", order=4): + """Integrates over all the faces of the given mesh. + + Parameters + ---------- + quadrature_rule : str, optional + Quadrature rule to use. Defaults to "triangular". + order : int, optional + Order of quadrature rule. Defaults to 4. + + Returns + ------- + Calculated integral : float + + Examples + -------- + Open a Uxarray dataset + + >>> import uxarray as ux + >>> uxds = ux.open_dataset("grid.ug", "centroid_pressure_data_ug") + + # Compute the integral + >>> integral = uxds.integrate() + """ + integral = 0.0 + + # call function to get area of all the faces as a np array + face_areas = self.uxgrid.compute_face_areas(quadrature_rule, order) + + # TODO: Should we fix this requirement? Shouldn't it be applicable to + # TODO: all variables of dataset or a dataarray instead? + var_key = list(self.keys()) + if len(var_key) > 1: + # warning: print message + print( + "WARNING: The dataset has more than one variable, using the first variable for integration" + ) + + var_key = var_key[0] + face_vals = self[var_key].to_numpy() + integral = np.dot(face_areas, face_vals) + + return integral + + def to_array(self) -> UxDataArray: + """Override to make the result an instance of + ``uxarray.UxDataArray``.""" + + xarr = super().to_array() + return UxDataArray(xarr, uxgrid=self.uxgrid) diff --git a/uxarray/grid.py b/uxarray/core/grid.py similarity index 50% rename from uxarray/grid.py rename to uxarray/core/grid.py index 9934bf70d..5fec06d2e 100644 --- a/uxarray/grid.py +++ b/uxarray/core/grid.py @@ -1,61 +1,65 @@ -"""uxarray grid module.""" -import os +"""uxarray.core.grid module.""" import xarray as xr import numpy as np # reader and writer imports -from ._exodus import _read_exodus, _encode_exodus -from ._ugrid import _read_ugrid, _encode_ugrid -from ._shapefile import _read_shpfile -from ._scrip import _read_scrip, _encode_scrip -from ._mpas import _read_mpas -from .helpers import get_all_face_area_from_coords, parse_grid_type, node_xyz_to_lonlat_rad, node_lonlat_rad_to_xyz, close_face_nodes -from .constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.io._exodus import _read_exodus, _encode_exodus +from uxarray.io._mpas import _read_mpas +from uxarray.io._ugrid import _read_ugrid, _encode_ugrid +from uxarray.io._shapefile import _read_shpfile +from uxarray.io._scrip import _read_scrip, _encode_scrip +from uxarray.utils.helpers import (get_all_face_area_from_coords, + parse_grid_type, node_xyz_to_lonlat_rad, + node_lonlat_rad_to_xyz, close_face_nodes) +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE class Grid: - """ + """Unstructured grid topology definition. + + Can be used standalone to explore an unstructured grid topology, or + can be seen as the property of ``uxarray.UxDataset`` and ``uxarray.DataArray`` + to make them unstructured grid-aware data sets and arrays. + + Parameters + ---------- + input_obj : xarray.Dataset, ndarray, list, tuple, required + Input ``xarray.Dataset`` or vertex coordinates that form faces. + + Other Parameters + ---------------- + gridspec: bool, optional + Specifies gridspec + islatlon : bool, optional + Specify if the grid is lat/lon based + isconcave: bool, optional + Specify if this grid has concave elements (internal checks for this are possible) + use_dual: bool, optional + Specify whether to use the primal (use_dual=False) or dual (use_dual=True) mesh if the file type is mpas + + Raises + ------ + RuntimeError + If specified file not found or recognized + Examples ---------- - Open an exodus file with Uxarray Grid object + >>> import uxarray as ux - >>> xarray_obj = xr.open_dataset("filename.g") - >>> mesh = ux.Grid(xarray_obj) + 1. Open a grid file with `uxarray.open_grid()`: - Encode as a `xarray.Dataset` in the UGRID format + >>> uxgrid = ux.open_grid("filename.g") - >>> mesh.encode_as("ugrid") - """ + 2. Open an unstructured grid dataset file with + `uxarray.open_dataset()`, then access `Grid` info: - def __init__(self, dataset, **kwargs): - """Initialize grid variables, decide if loading happens via file, verts - or gridspec. + >>> uxds = ux.open_dataset("filename.g") + """ - Parameters - ---------- - dataset : xarray.Dataset, ndarray, list, tuple, required - Input xarray.Dataset or vertex coordinates that form one face. - - Other Parameters - ---------------- - islatlon : bool, optional - Specify if the grid is lat/lon based - concave: bool, optional - Specify if this grid has concave elements (internal checks for this are possible) - gridspec: bool, optional - Specifies gridspec - mesh_type: str, optional - Specify the mesh file type, eg. exo, ugrid, shp, mpas, etc - use_dual: bool, optional - Specify whether to use the primal (use_dual=False) or dual (use_dual=True) mesh if the file type is mpas - Raises - ------ - RuntimeError - If specified file not found - """ + def __init__(self, input_obj, **kwargs): # initialize internal variable names - self.__init_ds_var_names__() + self.__init_grid_var_names__() # initialize face_area variable self._face_areas = None @@ -65,54 +69,55 @@ def __init__(self, dataset, **kwargs): # unpack kwargs # sets default values for all kwargs to None kwargs_list = [ - 'gridspec', 'vertices', 'islatlon', 'concave', 'source_grid', + 'gridspec', 'vertices', 'islatlon', 'isconcave', 'source_grid', 'use_dual' ] for key in kwargs_list: setattr(self, key, kwargs.get(key, None)) # check if initializing from verts: - if isinstance(dataset, (list, tuple, np.ndarray)): - dataset = np.asarray(dataset) + if isinstance(input_obj, (list, tuple, np.ndarray)): + input_obj = np.asarray(input_obj) + self.mesh_type = "From vertices" # grid with multiple faces - if dataset.ndim == 3: - self.__from_vert__(dataset) + if input_obj.ndim == 3: + self.__from_vert__(input_obj) self.source_grid = "From vertices" # grid with a single face - elif dataset.ndim == 2: - dataset = np.array([dataset]) - self.__from_vert__(dataset) + elif input_obj.ndim == 2: + input_obj = np.array([input_obj]) + self.__from_vert__(input_obj) self.source_grid = "From vertices" else: raise RuntimeError( - f"Invalid Input Dimension: {dataset.ndim}. Expected dimension should be " + f"Invalid Input Dimension: {input_obj.ndim}. Expected dimension should be " f"3: [nMesh2_face, nMesh2_node, Two/Three] or 2 when only " f"one face is passed in.") + # check if initializing from string # TODO: re-add gridspec initialization when implemented - elif isinstance(dataset, xr.Dataset): - self.mesh_type = parse_grid_type(dataset) - self.__from_ds__(dataset=dataset) + elif isinstance(input_obj, xr.Dataset): + self.mesh_type = parse_grid_type(input_obj) + self.__from_ds__(dataset=input_obj) else: raise RuntimeError("Dataset is not a valid input type.") - # initialize convenience attributes - self.__init_grid_var_attrs__() - - # construct connectivity - self._build_edge_node_connectivity() + # {"Standardized Name" : "Original Name"} + self._inverse_grid_var_names = { + v: k for k, v in self.grid_var_names.items() + } - # build face dimension, possibly safeguard for large datasets + # construct nNodes_per_Face self._build_nNodes_per_face() - def __init_ds_var_names__(self): + def __init_grid_var_names__(self): """Populates a dictionary for storing uxarray's internal representation of xarray object. Note ugrid conventions are flexible with names of variables, see: http://ugrid-conventions.github.io/ugrid-conventions/ """ - self.ds_var_names = { + self.grid_var_names = { "Mesh2": "Mesh2", "Mesh2_node_x": "Mesh2_node_x", "Mesh2_node_y": "Mesh2_node_y", @@ -124,41 +129,6 @@ def __init_ds_var_names__(self): "nMaxMesh2_face_nodes": "nMaxMesh2_face_nodes" } - def __init_grid_var_attrs__(self) -> None: - """Initialize attributes for directly accessing UGRID dimensions and - variables. - - Examples - ---------- - Assuming the mesh node coordinates for longitude are stored with an input - name of 'mesh_node_x', we store this variable name in the `ds_var_names` - dictionary with the key 'Mesh2_node_x'. In order to access it, we do: - - >>> x = grid.ds[grid.ds_var_names["Mesh2_node_x"]] - - With the help of this function, we can directly access it through the - use of a standardized name based on the UGRID conventions - - >>> x = grid.Mesh2_node_x - """ - - # Iterate over dict to set access attributes - for key, value in self.ds_var_names.items(): - # Set Attributes for Data Variables - if self.ds.data_vars is not None: - if value in self.ds.data_vars: - setattr(self, key, self.ds[value]) - - # Set Attributes for Coordinates - if self.ds.coords is not None: - if value in self.ds.coords: - setattr(self, key, self.ds[value]) - - # Set Attributes for Dimensions - if self.ds.dims is not None: - if value in self.ds.dims: - setattr(self, key, len(self.ds[value])) - def __from_vert__(self, dataset): """Create a grid with faces constructed from vertices specified by the given argument. @@ -168,8 +138,8 @@ def __from_vert__(self, dataset): dataset : ndarray, list, tuple, required Input vertex coordinates that form our face(s) """ - self.ds = xr.Dataset() - self.ds["Mesh2"] = xr.DataArray( + self._ds = xr.Dataset() + self._ds["Mesh2"] = xr.DataArray( attrs={ "cf_role": "mesh_topology", "long_name": "Topology data of unstructured mesh", @@ -179,7 +149,8 @@ def __from_vert__(self, dataset): "face_node_connectivity": "Mesh2_face_nodes", "face_dimension": "nMesh2_face" }) - self.ds.Mesh2.attrs['topology_dimension'] = dataset.ndim + + self._ds.Mesh2.attrs['topology_dimension'] = dataset.ndim if self.islatlon is not None and self.islatlon is False: x_units = 'm' @@ -229,25 +200,25 @@ def __from_vert__(self, dataset): indices[(indices > idx) & (indices != INT_FILL_VALUE)] -= 1 # Create coordinate DataArrays - self.ds["Mesh2_node_x"] = xr.DataArray(data=unique_verts[:, 0], - dims=["nMesh2_node"], - attrs={"units": x_units}) - self.ds["Mesh2_node_y"] = xr.DataArray(data=unique_verts[:, 1], - dims=["nMesh2_node"], - attrs={"units": y_units}) + self._ds["Mesh2_node_x"] = xr.DataArray(data=unique_verts[:, 0], + dims=["nMesh2_node"], + attrs={"units": x_units}) + self._ds["Mesh2_node_y"] = xr.DataArray(data=unique_verts[:, 1], + dims=["nMesh2_node"], + attrs={"units": y_units}) if dataset.shape[-1] > 2: - self.ds["Mesh2_node_z"] = xr.DataArray(data=unique_verts[:, 2], - dims=["nMesh2_node"], - attrs={"units": z_units}) + self._ds["Mesh2_node_z"] = xr.DataArray(data=unique_verts[:, 2], + dims=["nMesh2_node"], + attrs={"units": z_units}) else: - self.ds["Mesh2_node_z"] = xr.DataArray(data=unique_verts[:, 1] * - 0.0, - dims=["nMesh2_node"], - attrs={"units": z_units}) + self._ds["Mesh2_node_z"] = xr.DataArray(data=unique_verts[:, 1] * + 0.0, + dims=["nMesh2_node"], + attrs={"units": z_units}) # Create connectivity array using indices of unique vertices connectivity = indices.reshape(dataset.shape[:-1]) - self.ds["Mesh2_face_nodes"] = xr.DataArray( + self._ds["Mesh2_face_nodes"] = xr.DataArray( data=xr.DataArray(connectivity).astype(INT_DTYPE), dims=["nMesh2_face", "nMaxMesh2_face_nodes"], attrs={ @@ -261,24 +232,290 @@ def __from_ds__(self, dataset): """Loads a mesh dataset.""" # call reader as per mesh_type if self.mesh_type == "exo": - self.ds = _read_exodus(dataset, self.ds_var_names) + self._ds = _read_exodus(dataset, self.grid_var_names) elif self.mesh_type == "scrip": - self.ds = _read_scrip(dataset) + self._ds = _read_scrip(dataset) elif self.mesh_type == "ugrid": - self.ds, self.ds_var_names = _read_ugrid(dataset, self.ds_var_names) + self._ds, self.grid_var_names = _read_ugrid(dataset, + self.grid_var_names) elif self.mesh_type == "shp": - self.ds = _read_shpfile(dataset) + self._ds = _read_shpfile(dataset) elif self.mesh_type == "mpas": # select whether to use the dual mesh if self.use_dual is not None: - self.ds = _read_mpas(dataset, self.use_dual) + self._ds = _read_mpas(dataset, self.use_dual) else: - self.ds = _read_mpas(dataset) + self._ds = _read_mpas(dataset) else: raise RuntimeError("unknown mesh type") dataset.close() + def __repr__(self): + """Constructs a string representation of the contents of a ``Grid``.""" + + prefix = "\n" + original_grid_str = f"Original Grid Type: {self.mesh_type}\n" + dims_heading = "Grid Dimensions:\n" + dims_str = "" + # if self.grid_var_names["Mesh2_node_x"] in self._ds: + # dims_str += f" * nMesh2_node: {self.nMesh2_node}\n" + # if self.grid_var_names["Mesh2_face_nodes"] in self._ds: + # dims_str += f" * nMesh2_face: {self.nMesh2_face}\n" + # dims_str += f" * nMesh2_face: {self.nMesh2_face}\n" + + for key, value in zip(self._ds.dims.keys(), self._ds.dims.values()): + if key in self._inverse_grid_var_names: + dims_str += f" * {self._inverse_grid_var_names[key]}: {value}\n" + + if "nMesh2_edge" in self._ds.dims: + dims_str += f" * nMesh2_edge: {self.nMesh2_edge}\n" + + if "nMaxMesh2_face_edges" in self._ds.dims: + dims_str += f" * nMaxMesh2_face_edges: {self.nMaxMesh2_face_edges}\n" + + coord_heading = "Grid Coordinate Variables:\n" + coords_str = "" + if self.grid_var_names["Mesh2_node_x"] in self._ds: + coords_str += f" * Mesh2_node_x: {self.Mesh2_node_x.shape}\n" + coords_str += f" * Mesh2_node_y: {self.Mesh2_node_y.shape}\n" + if "Mesh2_node_cart_x" in self._ds: + coords_str += f" * Mesh2_node_cart_x: {self.Mesh2_node_cart_x.shape}\n" + coords_str += f" * Mesh2_node_cart_y: {self.Mesh2_node_cart_y.shape}\n" + coords_str += f" * Mesh2_node_cart_z: {self.Mesh2_node_cart_z.shape}\n" + if "Mesh2_face_x" in self._ds: + coords_str += f" * Mesh2_face_x: {self.Mesh2_face_x.shape}\n" + coords_str += f" * Mesh2_face_y: {self.Mesh2_face_y.shape}\n" + + connectivity_heading = "Grid Connectivity Variables:\n" + connectivity_str = "" + if self.grid_var_names["Mesh2_face_nodes"] in self._ds: + connectivity_str += f" * Mesh2_face_nodes: {self.Mesh2_face_nodes.shape}\n" + if "Mesh2_edge_nodes" in self._ds: + connectivity_str += f" * Mesh2_edge_nodes: {self.Mesh2_edge_nodes.shape}\n" + if "Mesh2_face_edges" in self._ds: + connectivity_str += f" * Mesh2_face_edges: {self.Mesh2_face_edges.shape}\n" + connectivity_str += f" * nNodes_per_face: {self.nNodes_per_face.shape}\n" + + return prefix + original_grid_str + dims_heading + dims_str + coord_heading + coords_str + \ + connectivity_heading + connectivity_str + + # attribute properties + + @property + def parsed_attrs(self): + """Dictionary of parsed attributes from the source grid.""" + return self._ds.attrs + + @property + def Mesh2(self): + """UGRID Attribute ``Mesh2``, which indicates the topology data of a 2D + unstructured mesh.""" + return self._ds[self.grid_var_names["Mesh2"]] + + # dimension properties + + @property + def nMesh2_node(self): + """UGRID Dimension ``nMesh2_node``, which represents the total number + of nodes.""" + return self._ds[self.grid_var_names["Mesh2_node_x"]].shape[0] + + @property + def nMesh2_face(self): + """UGRID Dimension ``nMesh2_face``, which represents the total number + of faces.""" + return self._ds[self.grid_var_names["Mesh2_face_nodes"]].shape[0] + + @property + def nMesh2_edge(self): + """UGRID Dimension ``nMesh2_edge``, which represents the total number + of edges.""" + + if "Mesh2_edge_nodes" not in self._ds: + self._build_edge_node_connectivity(repopulate=True) + + return self._ds['Mesh2_edge_nodes'].shape[0] + + @property + def nMaxMesh2_face_nodes(self): + """UGRID Dimension ``nMaxMesh2_face_nodes``, which represents the + maximum number of faces nodes that a face may contain.""" + return self.Mesh2_face_nodes.shape[1] + + @property + def nMaxMesh2_face_edges(self): + """Dimension ``nMaxMesh2_face_edges``, which represents the maximum + number of edges per face. + + Equivalent to ``nMaxMesh2_face_nodes`` + """ + + if "Mesh2_face_edges" not in self._ds: + self._build_face_edges_connectivity() + + return self._ds["Mesh2_face_edges"].shape[1] + + @property + def nNodes_per_face(self): + """Dimension Variable ``nNodes_per_face``, which contains the number of + non-fill-value nodes per face. + + Dimensions (``nMesh2_nodes``) and DataType ``INT_DTYPE``. + """ + return self._ds["nNodes_per_face"] + + # coordinate properties + + @property + def Mesh2_node_x(self): + """UGRID Coordinate Variable ``Mesh2_node_x``, which contains the + longitude of each node in degrees. + + Dimensions (``nMesh2_node``) + """ + return self._ds[self.grid_var_names["Mesh2_node_x"]] + + @property + def Mesh2_node_cart_x(self): + """Coordinate Variable ``Mesh2_node_cart_x``, which contains the x + location in meters. + + Dimensions (``nMesh2_node``) + """ + if "Mesh2_node_cart_x" not in self._ds: + self._populate_cartesian_xyz_coord() + return self._ds['Mesh2_node_cart_x'] + + @property + def Mesh2_face_x(self): + """UGRID Coordinate Variable ``Mesh2_face_x``, which contains the + longitude of each face center. + + Dimensions (``nMesh2_face``) + """ + if "Mesh2_face_x" in self._ds: + return self._ds["Mesh2_face_x"] + else: + return None + + @property + def Mesh2_node_y(self): + """UGRID Coordinate Variable ``Mesh2_node_y``, which contains the + latitude of each node. + + Dimensions (``nMesh2_node``) + """ + return self._ds[self.grid_var_names["Mesh2_node_y"]] + + @property + def Mesh2_node_cart_y(self): + """Coordinate Variable ``Mesh2_node_cart_y``, which contains the y + location in meters. + + Dimensions (``nMesh2_node``) + """ + if "Mesh2_node_cart_y" not in self._ds: + self._populate_cartesian_xyz_coord() + return self._ds['Mesh2_node_cart_y'] + + @property + def Mesh2_face_y(self): + """UGRID Coordinate Variable ``Mesh2_face_y``, which contains the + latitude of each face center. + + Dimensions (``nMesh2_face``) + """ + if "Mesh2_face_y" in self._ds: + return self._ds["Mesh2_face_y"] + else: + return None + + @property + def _Mesh2_node_z(self): + """Coordinate Variable ``_Mesh2_node_z``, which contains the level of + each node. It is only a placeholder for now as a protected attribute. + UXarray does not support this yet and only handles the 2D flexibile + meshes. + + If we introduce handling of 3D meshes in the future, it might be only + levels, i.e. the same level(s) for all nodes, instead of separate + level for each node that ``_Mesh2_node_z`` suggests. + + Dimensions (``nMesh2_node``) + """ + if self.grid_var_names["Mesh2_node_z"] in self._ds: + return self._ds[self.grid_var_names["Mesh2_node_z"]] + else: + return None + + @property + def Mesh2_node_cart_z(self): + """Coordinate Variable ``Mesh2_node_cart_z``, which contains the z + location in meters. + + Dimensions (``nMesh2_node``) + """ + if "Mesh2_node_cart_z" not in self._ds: + self._populate_cartesian_xyz_coord() + return self._ds['Mesh2_node_cart_z'] + + # connectivity properties + + @property + def Mesh2_face_nodes(self): + """UGRID Connectivity Variable ``Mesh2_face_nodes``, which maps each + face to its corner nodes. + + Dimensions (``nMesh2_face``, ``nMaxMesh2_face_nodes``) and + DataType ``INT_DTYPE``. + + Faces can have arbitrary length, with _FillValue=-1 used when faces + have fewer nodes than MaxNumNodesPerFace. + + Nodes are in counter-clockwise order. + """ + + return self._ds[self.grid_var_names["Mesh2_face_nodes"]] + + @property + def Mesh2_edge_nodes(self): + """UGRID Connectivity Variable ``Mesh2_edge_nodes``, which maps every + edge to the two nodes that it connects. + + Dimensions (``nMesh2_edge``, ``Two``) and DataType + ``INT_DTYPE``. + + Nodes are in arbitrary order. + """ + if "Mesh2_edge_nodes" not in self._ds: + self._build_edge_node_connectivity() + + return self._ds['Mesh2_edge_nodes'] + + @property + def Mesh2_face_edges(self): + """UGRID Connectivity Variable ``Mesh2_face_edges``, which maps every + face to its edges. + + Dimensions (``nMesh2_face``, ``nMaxMesh2_face_nodes``) and + DataType ``INT_DTYPE``. + """ + if "Mesh2_face_edges" not in self._ds: + self._build_face_edges_connectivity() + + return self._ds["Mesh2_face_edges"] + + def copy(self): + """Returns a deep copy of this grid.""" + return Grid(xr.Dataset(self._ds), + gridspec=self.gridspec, + vertices=self.vertices, + islatlon=self.islatlon, + isconcave=self.isconcave, + source_grid=self.source_grid, + use_dual=self.use_dual) + def encode_as(self, grid_type): """Encodes the grid as a new `xarray.Dataset` per grid format supplied in the `grid_type` argument. @@ -301,10 +538,10 @@ def encode_as(self, grid_type): """ if grid_type == "ugrid": - out_ds = _encode_ugrid(self.ds) + out_ds = _encode_ugrid(self._ds) elif grid_type == "exodus": - out_ds = _encode_exodus(self.ds, self.ds_var_names) + out_ds = _encode_exodus(self._ds, self.grid_var_names) elif grid_type == "scrip": out_ds = _encode_scrip(self.Mesh2_face_nodes, self.Mesh2_node_x, @@ -356,7 +593,7 @@ def compute_face_areas(self, quadrature_rule="triangular", order=4): >>> grid = ux.open_dataset("/home/jain/uxarray/test/meshfiles/ugrid/outCSne30/outCSne30.ug") - Get area of all faces in the same order as listed in grid.ds.Mesh2_face_nodes + Get area of all faces in the same order as listed in grid._ds.Mesh2_face_nodes >>> grid.face_areas array([0.00211174, 0.00211221, 0.00210723, ..., 0.00210723, 0.00211221, @@ -382,7 +619,7 @@ def compute_face_areas(self, quadrature_rule="triangular", order=4): y = self.Mesh2_node_y.data # check if z dimension if self.Mesh2.topology_dimension > 2: - z = self.Mesh2_node_z.data + z = self._Mesh2_node_z.data # Note: x, y, z are np arrays of type float # Using np.issubdtype to check if the type is float @@ -398,62 +635,106 @@ def compute_face_areas(self, quadrature_rule="triangular", order=4): return self._face_areas - # use the property keyword for declaration on face_areas property - @property - def face_areas(self): - """Declare face_areas as a property.""" - # if self._face_areas is not None: it allows for using the cached result - if self._face_areas is None: - self.compute_face_areas() - return self._face_areas - - def integrate(self, var_ds, quadrature_rule="triangular", order=4): - """Integrates over all the faces of the given mesh. + def __eq__(self, other): + """Two grids are equal if they have matching grid topology variables, + coordinates, and dims all of which are equal. Parameters ---------- - var_ds : Xarray dataset, required - Xarray dataset containing values to integrate on this grid - quadrature_rule : str, optional - Quadrature rule to use. Defaults to "triangular". - order : int, optional - Order of quadrature rule. Defaults to 4. + other : uxarray.Grid + The second grid object to be compared with `self` Returns ------- - Calculated integral : float + If two grids are equal : bool + """ + if other is not None: + # Iterate over dict to set access attributes + for key, value in self.grid_var_names.items(): + # Check if all grid variables are equal + if self._ds.data_vars is not None: + if value in self._ds.data_vars: + if not self._ds[value].equals( + other._ds[other.grid_var_names[key]]): + return False + else: + return False - Examples - -------- - Open grid file only + return True - >>> xr_grid = xr.open_dataset("grid.ug") - >>> grid = ux.Grid.(xr_grid) - >>> var_ds = xr.open_dataset("centroid_pressure_data_ug") + def __ne__(self, other): + """Two grids are not equal if they have differing grid topology + variables, coordinates, or dims. - # Compute the integral - >>> integral_psi = grid.integrate(var_ds) - """ - integral = 0.0 + Parameters + ---------- + other : uxarray.Grid + The second grid object to be compared with `self` - # call function to get area of all the faces as a np array - face_areas = self.compute_face_areas(quadrature_rule, order) + Returns + ------- + If two grids are not equal : bool + """ + return not self.__eq__(other) - var_key = list(var_ds.keys()) - if len(var_key) > 1: - # warning: print message - print( - "WARNING: The xarray dataset file has more than one variable, using the first variable for integration" - ) - var_key = var_key[0] - face_vals = var_ds[var_key].to_numpy() - integral = np.dot(face_areas, face_vals) + # use the property keyword for declaration on face_areas property + @property + def face_areas(self): + """Declare face_areas as a property.""" + # if self._face_areas is not None: it allows for using the cached result + if self._face_areas is None: + self.compute_face_areas() + return self._face_areas - return integral + # TODO: Make a decision on whether to provide Dataset- or DataArray-specific + # functions from within Grid + # def integrate(self, var_ds, quadrature_rule="triangular", order=4): + # """Integrates a xarray.Dataset over all the faces of the given mesh. + # + # Parameters + # ---------- + # var_ds : Xarray dataset, required + # Xarray dataset containing values to integrate on this grid + # quadrature_rule : str, optional + # Quadrature rule to use. Defaults to "triangular". + # order : int, optional + # Order of quadrature rule. Defaults to 4. + # + # Returns + # ------- + # Calculated integral : float + # + # Examples + # -------- + # Open grid file only + # + # >>> xr_grid = xr.open_dataset("grid.ug") + # >>> grid = ux.Grid.(xr_grid) + # >>> var_ds = xr.open_dataset("centroid_pressure_data_ug") + # + # # Compute the integral + # >>> integral_psi = grid.integrate(var_ds) + # """ + # integral = 0.0 + # + # # call function to get area of all the faces as a np array + # face_areas = self.compute_face_areas(quadrature_rule, order) + # + # var_key = list(var_ds.keys()) + # if len(var_key) > 1: + # # warning: print message + # print( + # "WARNING: The xarray dataset file has more than one variable, using the first variable for integration" + # ) + # var_key = var_key[0] + # face_vals = var_ds[var_key].to_numpy() + # integral = np.dot(face_areas, face_vals) + # + # return integral def _build_edge_node_connectivity(self, repopulate=False): """Constructs the UGRID connectivity variable (``Mesh2_edge_nodes``) - and stores it within the internal (``Grid.ds``) and through the + and stores it within the internal (``Grid._ds``) and through the attribute (``Grid.Mesh2_edge_nodes``). Additionally, the attributes (``inverse_indices``) and @@ -468,7 +749,7 @@ def _build_edge_node_connectivity(self, repopulate=False): """ # need to derive edge nodes - if "Mesh2_edge_nodes" not in self.ds or repopulate: + if "Mesh2_edge_nodes" not in self._ds or repopulate: padded_face_nodes = close_face_nodes(self.Mesh2_face_nodes.values, self.nMesh2_face, self.nMaxMesh2_face_nodes) @@ -518,7 +799,8 @@ def _build_edge_node_connectivity(self, repopulate=False): if inverse_indices[i] != INT_FILL_VALUE: inverse_indices[i] -= indexes[i] - self.ds['Mesh2_edge_nodes'] = xr.DataArray( + # add Mesh2_edge_nodes to internal dataset + self._ds['Mesh2_edge_nodes'] = xr.DataArray( edge_nodes_unique, dims=["nMesh2_edge", "Two"], attrs={ @@ -536,23 +818,19 @@ def _build_edge_node_connectivity(self, repopulate=False): fill_value_mask }) - # set standardized attributes - setattr(self, "Mesh2_edge_nodes", self.ds['Mesh2_edge_nodes']) - setattr(self, "nMesh2_edge", edge_nodes_unique.shape[0]) - def _build_face_edges_connectivity(self): """Constructs the UGRID connectivity variable (``Mesh2_face_edges``) - and stores it within the internal (``Grid.ds``) and through the + and stores it within the internal (``Grid._ds``) and through the attribute (``Grid.Mesh2_face_edges``).""" - if ("Mesh2_edge_nodes" not in self.ds or - "inverse_indices" not in self.ds['Mesh2_edge_nodes'].attrs): + if ("Mesh2_edge_nodes" not in self._ds or + "inverse_indices" not in self._ds['Mesh2_edge_nodes'].attrs): self._build_edge_node_connectivity(repopulate=True) - inverse_indices = self.ds['Mesh2_edge_nodes'].inverse_indices + inverse_indices = self._ds['Mesh2_edge_nodes'].inverse_indices inverse_indices = inverse_indices.reshape(self.nMesh2_face, self.nMaxMesh2_face_nodes) - self.ds["Mesh2_face_edges"] = xr.DataArray( + self._ds["Mesh2_face_edges"] = xr.DataArray( data=inverse_indices, dims=["nMesh2_face", "nMaxMesh2_face_edges"], attrs={ @@ -564,15 +842,11 @@ def _build_face_edges_connectivity(self): "Maps every edge to the two nodes that it connects", }) - # set standardized attributes - setattr(self, "nMaxMesh2_face_edges", inverse_indices.shape[1]) - setattr(self, "Mesh2_face_edges", self.ds["Mesh2_face_edges"]) - def _populate_cartesian_xyz_coord(self): - """A helper function that populates the xyz attribute in UXarray.ds. - This function is called when we need to use the cartesian coordinates - for each node to do the calculation but the input data only has the - "Mesh2_node_x" and "Mesh2_node_y" in degree. + """A helper function that populates the xyz attribute in + UXarray.Grid._ds. This function is called when we need to use the + cartesian coordinates for each node to do the calculation but the input + data only has the "Mesh2_node_x" and "Mesh2_node_y" in degree. Note ---- @@ -594,31 +868,31 @@ def _populate_cartesian_xyz_coord(self): """ # Check if the cartesian coordinates are already populated - if "Mesh2_node_cart_x" in self.ds.keys(): + if "Mesh2_node_cart_x" in self._ds.keys(): return - # check for units and create Mesh2_node_cart_x/y/z set to self.ds + # check for units and create Mesh2_node_cart_x/y/z set to self._ds nodes_lon_rad = np.deg2rad(self.Mesh2_node_x.values) nodes_lat_rad = np.deg2rad(self.Mesh2_node_y.values) nodes_rad = np.stack((nodes_lon_rad, nodes_lat_rad), axis=1) nodes_cart = np.asarray( list(map(node_lonlat_rad_to_xyz, list(nodes_rad)))) - self.ds["Mesh2_node_cart_x"] = xr.DataArray( + self._ds["Mesh2_node_cart_x"] = xr.DataArray( data=nodes_cart[:, 0], dims=["nMesh2_node"], attrs={ "standard_name": "cartesian x", "units": "m", }) - self.ds["Mesh2_node_cart_y"] = xr.DataArray( + self._ds["Mesh2_node_cart_y"] = xr.DataArray( data=nodes_cart[:, 1], dims=["nMesh2_node"], attrs={ "standard_name": "cartesian y", "units": "m", }) - self.ds["Mesh2_node_cart_z"] = xr.DataArray( + self._ds["Mesh2_node_cart_z"] = xr.DataArray( data=nodes_cart[:, 2], dims=["nMesh2_node"], attrs={ @@ -658,32 +932,32 @@ def _populate_lonlat_coord(self): """ # Check if the "Mesh2_node_x" is already in longitude - if "degree" in self.ds.Mesh2_node_x.units: + if "degree" in self._ds.Mesh2_node_x.units: return # Check if the input Mesh2_node_xyz" are represented in the cartesian format with the unit "m" - if ("m" not in self.ds.Mesh2_node_x.units) or ("m" not in self.ds.Mesh2_node_y.units) \ - or ("m" not in self.ds.Mesh2_node_z.units): + if ("m" not in self._ds.Mesh2_node_x.units) or ("m" not in self._ds.Mesh2_node_y.units) \ + or ("m" not in self._ds.Mesh2_node_z.units): raise RuntimeError( "Expected: Mesh2_node_x/y/z should be represented in the cartesian format with the " "unit 'm' when calling this function") # Put the cartesian coordinates inside the proper data structure - self.ds["Mesh2_node_cart_x"] = xr.DataArray( - data=self.ds["Mesh2_node_x"].values) - self.ds["Mesh2_node_cart_y"] = xr.DataArray( - data=self.ds["Mesh2_node_y"].values) - self.ds["Mesh2_node_cart_z"] = xr.DataArray( - data=self.ds["Mesh2_node_z"].values) + self._ds["Mesh2_node_cart_x"] = xr.DataArray( + data=self._ds["Mesh2_node_x"].values) + self._ds["Mesh2_node_cart_y"] = xr.DataArray( + data=self._ds["Mesh2_node_y"].values) + self._ds["Mesh2_node_cart_z"] = xr.DataArray( + data=self._ds["Mesh2_node_z"].values) # convert the input cartesian values into the longitude latitude degree nodes_cart = np.stack( - (self.ds["Mesh2_node_x"].values, self.ds["Mesh2_node_y"].values, - self.ds["Mesh2_node_z"].values), + (self._ds["Mesh2_node_x"].values, self._ds["Mesh2_node_y"].values, + self._ds["Mesh2_node_z"].values), axis=1).tolist() nodes_rad = list(map(node_xyz_to_lonlat_rad, nodes_cart)) nodes_degree = np.rad2deg(nodes_rad) - self.ds["Mesh2_node_x"] = xr.DataArray( + self._ds["Mesh2_node_x"] = xr.DataArray( data=nodes_degree[:, 0], dims=["nMesh2_node"], attrs={ @@ -691,7 +965,7 @@ def _populate_lonlat_coord(self): "long_name": "longitude of mesh nodes", "units": "degrees_east", }) - self.ds["Mesh2_node_y"] = xr.DataArray( + self._ds["Mesh2_node_y"] = xr.DataArray( data=nodes_degree[:, 1], dims=["nMesh2_node"], attrs={ @@ -713,10 +987,7 @@ def _build_nNodes_per_face(self): nNodes_per_face = np.argmax(closed == INT_FILL_VALUE, axis=1) # add to internal dataset - self.ds["nNodes_per_face"] = xr.DataArray( + self._ds["nNodes_per_face"] = xr.DataArray( data=nNodes_per_face, dims=["nMesh2_face"], attrs={"long_name": "number of non-fill value nodes for each face"}) - - # standardized attribute - setattr(self, "nNodes_per_face", self.ds["nNodes_per_face"]) diff --git a/uxarray/io/__init__.py b/uxarray/io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/uxarray/_exodus.py b/uxarray/io/_exodus.py similarity index 92% rename from uxarray/_exodus.py rename to uxarray/io/_exodus.py index d6af036e6..49eea65f3 100644 --- a/uxarray/_exodus.py +++ b/uxarray/io/_exodus.py @@ -3,12 +3,12 @@ from pathlib import PurePath from datetime import datetime -from uxarray.helpers import _replace_fill_values -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.helpers import _replace_fill_values +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE # Exodus Number is one-based. -def _read_exodus(ext_ds, ds_var_names): +def _read_exodus(ext_ds, grid_var_names): """Exodus file reader. Parameters: xarray.Dataset, required @@ -111,7 +111,8 @@ def _read_exodus(ext_ds, ds_var_names): dtype=conn.dtype) conn = value.data else: - raise "found face_nodes_dim greater than nMaxMesh2_face_nodes" + raise RuntimeError( + "found face_nodes_dim greater than nMaxMesh2_face_nodes") # find the elem_type as etype for this element for k, v in value.attrs.items(): @@ -151,7 +152,7 @@ def _read_exodus(ext_ds, ds_var_names): return ds -def _encode_exodus(ds, ds_var_names, outfile=None): +def _encode_exodus(ds, grid_var_names, outfile=None): """Encodes an Exodus file. Parameters @@ -211,32 +212,32 @@ def _encode_exodus(ds, ds_var_names, outfile=None): dims=["four", "num_qa_rec"]) # get orig dimension from Mesh2 attribute topology dimension - dim = ds[ds_var_names["Mesh2"]].topology_dimension + dim = ds[grid_var_names["Mesh2"]].topology_dimension c_data = [] if dim == 2: c_data = xr.DataArray([ - ds[ds_var_names["Mesh2_node_x"]].data.tolist(), - ds[ds_var_names["Mesh2_node_y"]].data.tolist() + ds[grid_var_names["Mesh2_node_x"]].data.tolist(), + ds[grid_var_names["Mesh2_node_y"]].data.tolist() ]) elif dim == 3: c_data = xr.DataArray([ - ds[ds_var_names["Mesh2_node_x"]].data.tolist(), - ds[ds_var_names["Mesh2_node_y"]].data.tolist(), - ds[ds_var_names["Mesh2_node_z"]].data.tolist() + ds[grid_var_names["Mesh2_node_x"]].data.tolist(), + ds[grid_var_names["Mesh2_node_y"]].data.tolist(), + ds[grid_var_names["Mesh2_node_z"]].data.tolist() ]) exo_ds["coord"] = xr.DataArray(data=c_data, dims=["num_dim", "num_nodes"]) # process face nodes, this array holds num faces at corresponding location # eg num_el_all_blks = [0, 0, 6, 12] signifies 6 TRI and 12 SHELL elements - num_el_all_blks = np.zeros(ds[ds_var_names["nMaxMesh2_face_nodes"]].size, + num_el_all_blks = np.zeros(ds[grid_var_names["nMaxMesh2_face_nodes"]].size, "i8") # this list stores connectivity without filling conn_nofill = [] # store the number of faces in an array - for row in ds[ds_var_names["Mesh2_face_nodes"]].astype(INT_DTYPE).data: + for row in ds[grid_var_names["Mesh2_face_nodes"]].astype(INT_DTYPE).data: # find out -1 in each row, this indicates lower than max face nodes arr = np.where(row == -1) @@ -252,7 +253,7 @@ def _encode_exodus(ds, ds_var_names, outfile=None): conn_nofill.append(list_node) elif arr[0].size == 0: # increment the number of faces for this "nMaxMesh2_face_nodes" face - num_el_all_blks[ds[ds_var_names["nMaxMesh2_face_nodes"]].size - + num_el_all_blks[ds[grid_var_names["nMaxMesh2_face_nodes"]].size - 1] += 1 # get integer list nodes list_node = list(map(int, row.tolist())) diff --git a/uxarray/_mpas.py b/uxarray/io/_mpas.py similarity index 98% rename from uxarray/_mpas.py rename to uxarray/io/_mpas.py index ebc33c6c9..a42f3b9cb 100644 --- a/uxarray/_mpas.py +++ b/uxarray/io/_mpas.py @@ -2,7 +2,7 @@ import numpy as np import warnings -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE def _primal_to_ugrid(in_ds, out_ds): @@ -245,14 +245,14 @@ def _set_global_attrs(in_ds, out_ds): # typically a random string used for tracking mesh provenance if 'mesh_id' in in_ds.attrs: out_ds.attrs['mesh_id'] = in_ds.mesh_id - else: - warnings.warn("Missing Required Attribute: 'mesh_id'") + # else: + # warnings.warn("Missing Required Attribute: 'mesh_id'") # defines the version of the MPAS Mesh specification the mesh conforms to if 'mesh_spec' in in_ds.attrs: out_ds.attrs['mesh_spec'] = in_ds.mesh_spec - else: - warnings.warn("Missing Required Attribute: 'mesh_spec'") + # else: + # warnings.warn("Missing Required Attribute: 'mesh_spec'") # defines if the mesh describes points that lie on the surface of a sphere or not if "on_a_sphere" in in_ds.attrs: diff --git a/uxarray/_scrip.py b/uxarray/io/_scrip.py similarity index 98% rename from uxarray/_scrip.py rename to uxarray/io/_scrip.py index 7ab4b3bfa..68a16338a 100644 --- a/uxarray/_scrip.py +++ b/uxarray/io/_scrip.py @@ -1,10 +1,9 @@ import xarray as xr import numpy as np -from uxarray.helpers import grid_center_lat_lon +from uxarray.utils.helpers import grid_center_lat_lon, _replace_fill_values -from uxarray.helpers import _replace_fill_values -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE def _to_ugrid(in_ds, out_ds): diff --git a/uxarray/_shapefile.py b/uxarray/io/_shapefile.py similarity index 100% rename from uxarray/_shapefile.py rename to uxarray/io/_shapefile.py diff --git a/uxarray/_ugrid.py b/uxarray/io/_ugrid.py similarity index 96% rename from uxarray/_ugrid.py rename to uxarray/io/_ugrid.py index f6b237038..42e298aa7 100644 --- a/uxarray/_ugrid.py +++ b/uxarray/io/_ugrid.py @@ -1,7 +1,7 @@ import xarray as xr import numpy as np -from uxarray.helpers import _replace_fill_values -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.helpers import _replace_fill_values +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE def _read_ugrid(xr_ds, var_names_dict): diff --git a/uxarray/utils/__init__.py b/uxarray/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/uxarray/constants.py b/uxarray/utils/constants.py similarity index 100% rename from uxarray/constants.py rename to uxarray/utils/constants.py diff --git a/uxarray/get_quadratureDG.py b/uxarray/utils/get_quadratureDG.py similarity index 100% rename from uxarray/get_quadratureDG.py rename to uxarray/utils/get_quadratureDG.py diff --git a/uxarray/helpers.py b/uxarray/utils/helpers.py similarity index 99% rename from uxarray/helpers.py rename to uxarray/utils/helpers.py index 2453c80ee..caf18b77c 100644 --- a/uxarray/helpers.py +++ b/uxarray/utils/helpers.py @@ -5,7 +5,7 @@ from numba import njit, config import math -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.utils.constants import INT_DTYPE, INT_FILL_VALUE config.DISABLE_JIT = False