From 00c5e40b6ce95bfe66c302c766f86c3a14bdbde4 Mon Sep 17 00:00:00 2001 From: tomsail Date: Tue, 21 May 2024 15:14:55 +0200 Subject: [PATCH] corrected Meteo_chunks Notebook --- Meteo_Chunks.v0.ipynb | 1063 ----- Meteo_Chunks.v0.html => Meteo_Chunks.v1.html | 319 +- Meteo_Chunks.v1.ipynb | 4439 ++++++++++++++++++ index.html | 2 +- 4 files changed, 4647 insertions(+), 1176 deletions(-) delete mode 100644 Meteo_Chunks.v0.ipynb rename Meteo_Chunks.v0.html => Meteo_Chunks.v1.html (97%) create mode 100644 Meteo_Chunks.v1.ipynb diff --git a/Meteo_Chunks.v0.ipynb b/Meteo_Chunks.v0.ipynb deleted file mode 100644 index 27b63e5..0000000 --- a/Meteo_Chunks.v0.ipynb +++ /dev/null @@ -1,1063 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Meteo conversion optimisation for hydro models\n", - "\n", - "We will go in details of how to convert meteorological data (ECMWF/ERA5): \n", - " * from netcdf gridded data \n", - "\n", - " ![ECMWF](https://climate.copernicus.eu/sites/default/files/inline-images/CERRA_wind.png)\n", - " * onto zarr unstrutured meshes format \n", - " \n", - " ![global mesh](assets/ERA5_wind.gif)\n", - "\n", - "The idea is to able to prepare the data to be able to use it as an input for our hydro models (SCHISM & TELEMAC) used at JRC" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1 - Sensitivity on the chunking size\n", - "\n", - "### Important! first write an empty array\n", - "> Dimensions cannot be included in both `region` and `append_dim` at the same time. To create empty arrays to fill in with `region`, use a separate call to `to_zarr()` with `compute=False.`\n", - "\n", - "the first step consists in creating an initial Zarr dummy Dataset array." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import dask.array as da\n", - "import numpy as np\n", - "import hvplot.pandas\n", - "import pandas as pd\n", - "import holoviews as hv\n", - "from holoviews import opts\n", - "import pandas as pd\n", - "import xarray as xr\n", - "import shutil\n", - "import os\n", - "import time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set up parameters for the dataset\n", - "NTIMES = 744 # one month hourly\n", - "NNODES = 100000 # 100k nodes\n", - "chunk_size_time = 10 # Choose an appropriate chunk size for the time dimension" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "zero = da.empty((NTIMES, NNODES), chunks=(chunk_size_time, NNODES))\n", - "zero" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "what about if we had a dimension for the variables?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "NVAR = 4\n", - "cube_zero = da.empty((NTIMES,NVAR, NNODES), chunks=(chunk_size_time, 1, NNODES))\n", - "cube_zero" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Write the data to a temporary Zarr store\n", - "tmp = 'tmp.zarr'\n", - "ds = xr.Dataset({\n", - " \"u10\": (('time', 'node'), zero),\n", - " \"v10\": (('time', 'node'), zero),\n", - " \"msl\": (('time', 'node'), zero),\n", - " \"tmp\": (('time', 'node'), zero),\n", - " \n", - "})\n", - "ds.to_zarr(tmp, compute=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "we generated a metadata file, which will help us to aggregate the data later on. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!cat tmp.zarr/.zmetadata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "we'll put everything in a function to calculate the time taken to write data, depending on : \n", - " * the chunk size on the `time` dimention \n", - " * the chunk size on the `node` dimention " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def test_chunk_size(time_chunk, node_chunk):\n", - " start = time.time()\n", - " tmp = 'tmp.zarr'\n", - " if os.path.exists(tmp):\n", - " shutil.rmtree(tmp)\n", - " # create attrs and file \n", - " zero = da.empty((NTIMES, NNODES), chunks=(time_chunk, node_chunk))\n", - " bytes_per_element = zero.dtype.itemsize\n", - " chunk_size_mb = time_chunk * node_chunk * bytes_per_element / 1024 / 1000\n", - " if chunk_size_mb > 2000: \n", - " return np.nan, np.nan\n", - " else: \n", - " ds = xr.Dataset({\n", - " \"u10\": (('time', 'node'), zero),\n", - " \"v10\": (('time', 'node'), zero),\n", - " \"msl\": (('time', 'node'), zero),\n", - " \"tmp\": (('time', 'node'), zero), \n", - " }).to_zarr(tmp, compute=False)\n", - " # Create a Pandas date range for the time coordinates\n", - " time_range = pd.date_range(start=pd.Timestamp(2023, 1, 1), periods=NTIMES, freq='h')\n", - " # Loop over variables\n", - " count = 0\n", - " for var_name in [\"u10\", \"v10\", \"msl\", \"tmp\"]:\n", - " # Loop over time in chunks\n", - " for t_start in range(0, NTIMES, time_chunk):\n", - " t_end = min(t_start + time_chunk, NTIMES)\n", - " time_chunk = time_range[t_start:t_end]\n", - " # Loop over nodes in chunks\n", - " for i_node in range(0, NNODES, node_chunk):\n", - " end_node = min(i_node + node_chunk, NNODES)\n", - " data_chunk = np.ones((t_end - t_start) * (end_node - i_node))\n", - " data_chunk = data_chunk.reshape((t_end - t_start, end_node - i_node))\n", - " node_chunk = np.arange(i_node, end_node)\n", - " coords = {'node': node_chunk, 'time':time_chunk}\n", - " ds = xr.Dataset({var_name: (('time', 'node'), data_chunk)}, coords=coords)\n", - " region = {'time': slice(t_start, t_end), 'node': slice(i_node, end_node)}\n", - " ds.to_zarr(tmp, mode=\"a-\", region=region)\n", - " count += 1\n", - " \n", - " end = time.time()\n", - " print(f\"Chunk size: times:{time_chunk} x nodes:{node_chunk} - {chunk_size_mb}kB\")\n", - " return end - start, chunk_size_mb\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Iterate for a range of chunk sizes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "chunksizes = [10, 50, 100, 150, 200, 250, 300, 350, 400, 500]\n", - "nodessizes = [5000, 10000, 50000, 100000] #, 500000, 1000000]\n", - "\n", - "time_perf = np.zeros((len(chunksizes), len(nodessizes)))\n", - "sizes = np.zeros((len(chunksizes), len(nodessizes)))\n", - "for i_t, chunktime in enumerate(chunksizes): \n", - " for i_n, chunknode in enumerate(nodessizes):\n", - " perf, size = test_chunk_size(chunktime, chunknode)\n", - " time_perf[i_t,i_n] = perf\n", - " sizes[i_t,i_n] = size" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Convert data for pcolormesh plots into a format compatible with hvplot\n", - "time_perf_mesh_df = pd.DataFrame(time_perf, index=chunksizes, columns=nodessizes)\n", - "sizes_mesh_df = pd.DataFrame(sizes, index=chunksizes, columns=nodessizes)\n", - "\n", - "# Convert data for line plot into a Pandas DataFrame\n", - "sizes_flat = sizes.ravel()\n", - "time_perf_flat = time_perf.ravel()\n", - "idx = np.argsort(sizes_flat)\n", - "line_df = pd.DataFrame({\n", - " 'Size (MB)': sizes_flat[idx],\n", - " 'Time (sec)': time_perf_flat[idx]\n", - "})\n", - "\n", - "# Create hvplot pcolormesh plots\n", - "time_perf_plot = time_perf_mesh_df.hvplot.heatmap(\n", - " x='columns', y='index', C='value', logx=True, colorbar=True, xlabel=\"chunk size for nodes\", ylabel = \"chunk size for times\", \n", - " title='Time taken for each chunk (in seconds)', width = 500)\n", - "\n", - "sizes_plot = sizes_mesh_df.hvplot.heatmap(\n", - " x='columns', y='index', C='value', logx=True, colorbar=True, xlabel=\"chunk size for nodes\", ylabel = \"chunk size for times\", \n", - " title='Size of each chunk (in MB)', width = 500)\n", - "\n", - "# Create hvplot line plot\n", - "line_plot = line_df.hvplot.line(\n", - " x='Size (MB)', y='Time (sec)', \n", - " title='Time taken vs. Size of each chunk', width = 500)\n", - "\n", - "# Combine plots into a layout\n", - "layout = (time_perf_plot + sizes_plot + line_plot).cols(3)\n", - "\n", - "# Apply some options for better layout\n", - "layout.opts(\n", - " opts.HeatMap(colorbar=True),\n", - " opts.Layout(shared_axes=False, merge_tools=False),\n", - ")\n", - "# # Display the layout\n", - "hv.output(layout)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Conclusion: that the bigger the chunk size is, the smaller is the time to process the data. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2 - test with real data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We used previously the `region` parameter to append chunks to the zarr file.\n", - "\n", - "According to the [docs](https://docs.xarray.dev/en/stable/user-guide/io.html#modifying-existing-zarr-stores), \n", - "> region (`dict` or `\"auto\"`, optional) – Optional mapping from dimension names to integer slices along dataset dimensions to indicate the region of existing zarr array(s) in which to write this dataset’s data. For example, `{'x': slice(0, 1000), 'y': slice(10000, 11000)}` would indicate that values should be written to the region `0:1000` along `x` and `10000:11000` along `y`.\n", - "\n", - "**Conclusion**: We can only enter **monotonic** slices in the `region` dictionary argument. That means we need specify the output chunks with monotomic slices of a **constant size**.\n", - "\n", - "To have coherent geographic we need to reorder the nodes along their coordinates" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.1 load mesh file\n", - "we will use the `xarray-selafin` package to load the mesh file" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! poetry add xarray-selafin" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# load mesh file\n", - "mesh = xr.open_dataset(\"global-v0.slf\", engine = 'selafin')\n", - "mesh" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.2 split and reorder along regions " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "let's visualise the mesh nodes number along the lat/lon dimensions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_mesh(x, y): \n", - " df = pd.DataFrame({'x': x, 'y': y, 'id': np.arange(len(x))})\n", - " im = df.hvplot.points(x='x', y='y', c='id',s=10)\n", - " return im\n", - "\n", - "x, y = mesh.x.values, mesh.y.values\n", - "plot_mesh(x,y).opts(width = 1200, height = 600,cmap='tab20c')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "out of the mesher, mesh nodes are not sorted, we need to reorder them.\n", - "\n", - "The methodology will proceed as following : \n", - "\n", - "1. Normalize the longitude (x) and latitude (y) coordinates of the nodes.\n", - "2. Compute the ordering weights for the nodes.\n", - "3. For each region, determine which nodes fall within that region.\n", - "4. Reorder the nodes within each region based on the computed weights.\n", - "5. Remap the connectivity of the triangles to reflect the new node ordering." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "first we'll load the regions from a `json` file we previously created on `QGIS`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "with open(\"world_regions.json\") as f:\n", - " json_regions = json.load(f)\n", - "json_regions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import geopandas as gpd\n", - "import hvplot.pandas # This import is necessary to use the hvplot accessor\n", - "countries = gpd.read_file(gpd.datasets.get_path(\"naturalearth_lowres\"))\n", - "world_regions = gpd.read_file(\"world_regions.json\")\n", - "countries_plot = countries.hvplot(color='k')\n", - "world_regions_plot = world_regions.hvplot(\n", - " alpha=0.6, \n", - " c='id', \n", - " cmap='tab20c',\n", - " hover_cols=['id','name'],)\n", - "overlay = countries_plot * world_regions_plot\n", - "overlay.opts(\n", - " width = 1200,\n", - " height = 600,\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "3 functions below: \n", - "1. `reorder_nodes_within_region`: reorder the nodes within a given region based on the computed weights.\n", - "2. `remap_connectivity`: remap the connectivity of the triangles to reflect the new node ordering.\n", - "3. `reorder_mesh`: main functions that translates input mesh to \"ordered\" mesh. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Tuple, List, Iterable\n", - "import numpy_indexed as npi\n", - "\n", - "def remap_connectivity(\n", - " tri: np.ndarray, \n", - " mapping: np.ndarray\n", - " ) -> np.ndarray:\n", - " \"\"\"Remap the connectivity of a triangular mesh based on the new node order.\n", - "\n", - " Args:\n", - " tri: The original connectivity array of the triangular mesh.\n", - " mapping: The array that maps old node indices to new ones.\n", - "\n", - " Returns:\n", - " The remapped connectivity array for the triangular mesh.\n", - " \"\"\" \n", - " remapped_nodes = np.arange(len(mapping))\n", - " remapped_triface_nodes = np.c_[\n", - " npi.remap(tri[:, 0], mapping, remapped_nodes),\n", - " npi.remap(tri[:, 1], mapping, remapped_nodes),\n", - " npi.remap(tri[:, 2], mapping, remapped_nodes),\n", - " ]\n", - " return remapped_triface_nodes\n", - "\n", - "\n", - "def reorder_nodes_within_region(\n", - " x: np.ndarray, \n", - " y: np.ndarray, \n", - " region_polygon: gpd.GeoDataFrame, \n", - " order_wgts: np.ndarray\n", - " ) -> np.ndarray:\n", - " \"\"\"Reorder nodes within a given region based on their weights.\n", - "\n", - " Args:\n", - " x: The x-coordinates of the nodes.\n", - " y: The y-coordinates of the nodes.\n", - " region_polygon: The polygon representing the region.\n", - " order_wgts: The weights for ordering the nodes.\n", - "\n", - " Returns:\n", - " The indices of the reordered nodes within the given region.\n", - " \"\"\" \n", - " bbox = region_polygon.bounds\n", - " points_in_region = (y >= bbox[1]) & (y <= bbox[3]) & (x >= bbox[0]) & (x <= bbox[2])\n", - " indices_in_region = np.where(points_in_region)[0]\n", - " order_wgts_in_region = order_wgts[indices_in_region]\n", - " idx_sort = np.argsort(order_wgts_in_region)\n", - " mapping = np.arange(len(x))\n", - " mapping[indices_in_region] = indices_in_region[idx_sort]\n", - " return indices_in_region[idx_sort]\n", - "\n", - "\n", - "def reorder_mesh(\n", - " x: np.ndarray, \n", - " y: np.ndarray, \n", - " tri:np.ndarray, \n", - " regions: gpd.GeoDataFrame\n", - " ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[int]]:\n", - " \"\"\"Reorder the mesh nodes and remap the connectivity for each region.\n", - "\n", - " Args:\n", - " mesh: The dataset representing the mesh.\n", - " regions: A GeoDataFrame representing the regions.\n", - "\n", - " Returns:\n", - " A tuple containing the reordered x-coordinates, y-coordinates, \n", - " remapped connectivity, and the global sorting indices.\n", - " \"\"\" # 1 normalise\n", - " normalized_lon = mesh.x - np.min(mesh.x)\n", - " normalized_lat = mesh.y - np.min(mesh.y)\n", - " # 2 compute ordering\n", - " order_wgts = (normalized_lon) + (180-normalized_lat) * 360\n", - " # 3 test point in regions and fill in mapping / sorted indices\n", - " global_sorted = []\n", - " for ir, region in regions.iterrows():\n", - " region_polygon = region['geometry']\n", - " # 4. Reorder the nodes within each region \n", - " sorted_indices = reorder_nodes_within_region(x,y, region_polygon, order_wgts)\n", - " global_sorted.extend(sorted_indices)\n", - " # 5. Remap the connectivity \n", - " tri_out = remap_connectivity(tri, np.array(global_sorted))\n", - " return x[global_sorted], y[global_sorted], tri_out, global_sorted\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x, y, tri = mesh.x.values, mesh.y.values, mesh.attrs['ikle2'] - 1\n", - "x_, y_, tri_, map_ = reorder_mesh(x, y, tri, world_regions)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "test if the renumbering worked as expected: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plot_mesh(x_, y_).opts(width = 1200, height = 600, cmap = 'tab20c')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "test if the triangle remapping worked as expected: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "fig, ax = plt.subplots(1,1, figsize = (12,6))\n", - "def is_overlapping(tris, meshx):\n", - " PIR = 180\n", - " x1, x2, x3 = meshx[tris].T\n", - " return np.logical_or(abs(x2 - x1) > PIR, abs(x3 - x1) > PIR, abs(x3 - x2) > PIR)\n", - "m = is_overlapping(tri_ ,x_)\n", - "ax.tricontourf(x_, y_, tri_[~m], np.arange(len(x)),cmap = 'tab20c')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### finalise and save the new mesh dataset: " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "using [thalassa](https://github.com/ec-jrc/Thalassa/blob/b9d977cd6999e73f5ad884e9e7b96d4041b60827/thalassa/normalization.py#L27)'s `GENERIC` Format" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mesh_out = xr.Dataset({\n", - " 'lon': (['node'], x_),\n", - " 'lat': (['node'], y_),\n", - " 'triface_nodes': (['triface', 'three'], tri_),\n", - " 'depth': (['node'], mesh.B.isel(time=0).values[map_]),\n", - "})\n", - "mesh_out" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "check the depth assignation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "fig, ax = plt.subplots(1,1, figsize = (12,6))\n", - "ax.tricontourf(x_, y_, tri_[~m], mesh_out.depth)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.3 interpolate meteo and append on to zarr file" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "we will load ERA5 reanalysis data and test the chunking size on the `time` and `node` dimensions: \n", - " * for the `time` dimension, the chunking is quite straightforward.\n", - " * for the `node` dimension, the chunking gets more complex because we deal with 2 different meshes. we will explain why in details below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# load ERA5 reanalysis data\n", - "era5 = xr.open_dataset(\"era5_2023_uvp.nc\")\n", - "era5" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "first we will the interpolation functions used in TELEMAC ( [`optim_gen-atm`](https://gitlab.pam-retd.fr/otm/telemac-mascaret/-/blob/optim-gen-atm/scripts/python3/utils/geometry.py) branch)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.spatial import Delaunay, cKDTree\n", - "def get_weights(in_xy, out_xy, d=2):\n", - " t = Delaunay(in_xy) # triangulate output mesh\n", - " s = t.find_simplex(out_xy) \n", - " vert = np.take(t.simplices, np.maximum(s, 0), axis=0) # Use max to avoid negative indices\n", - " t_ = np.take(t.transform, np.maximum(s, 0), axis=0)\n", - " delta = out_xy - t_[:, d]\n", - " bary = np.einsum('njk,nk->nj', t_[:, :d, :], delta)\n", - " wgts = np.hstack((bary, 1 - bary.sum(axis=1, keepdims=True)))\n", - " # Points outside the out_xy\n", - " out_idx_out = s < 0 \n", - " if np.any(out_idx_out):\n", - " # For points outside, find nearest neighbors\n", - " tree = cKDTree(in_xy)\n", - " _, in_idx_out = tree.query(out_xy[out_idx_out])\n", - " else : \n", - " in_idx_out = None\n", - " return vert, wgts, out_idx_out, in_idx_out\n", - "\n", - "\n", - "def interp(values, vtx, wts, out_idx_out, in_idx_out):\n", - " res = np.einsum('nj,nj->n', np.take(values, vtx), wts)\n", - " if in_idx_out is not None:\n", - " res[out_idx_out] = values[in_idx_out]\n", - " return res" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "here we will mimic the first function from above in order to append to a zarr store" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "# Function to subset ERA data based on the mesh extent\n", - "def subset_era_from_mesh(\n", - " era: xr.Dataset, \n", - " mesh: xr.Dataset, \n", - " input360: bool,\n", - " gtype:str,\n", - ") -> xr.Dataset:\n", - " \"\"\"\n", - " Selects a subset of ERA data that overlaps with the provided mesh's geographical extent.\n", - "\n", - " :param era: The ERA dataset from which to select a subset.\n", - " :param mesh: The mesh dataset defining the geographical extent for the subset selection.\n", - " :param input360: A flag indicating whether the longitude should be adjusted to a 0-360 range.\n", - " :return: A subset of the ERA dataset that falls within the mesh's geographical extent.\n", - " \"\"\"\n", - " xmin, xmax, ymin, ymax = mesh.lon.min(), mesh.lon.max(), mesh.lat.min(), mesh.lat.max()\n", - " if input360:\n", - " xmin, xmax = np.mod(xmin+360, 360), np.mod(xmax+360, 360)\n", - " if xmax < xmin:\n", - " xmin, xmax = 0, 360\n", - " if gtype == \"grid\":\n", - " era_chunk = era.sel(longitude=slice(xmin, xmax), latitude=slice(ymax, ymin))\n", - " else: # for 01280 grid\n", - " mask_lon = (era.longitude >= xmin) & (era.longitude <= xmax)\n", - " mask_lat = (era.latitude >= ymin) & (era.latitude <= ymax)\n", - " mask = mask_lon & mask_lat\n", - " indices = np.where(mask)[0]\n", - " era_chunk = era.isel(values=indices)\n", - " return era_chunk\n", - "\n", - "# Function to write meteorological data onto a mesh\n", - "def write_meteo_on_mesh(\n", - " era_ds: xr.Dataset, \n", - " mesh: xr.Dataset, \n", - " file_out: str, \n", - " n_time_chunk: int, \n", - " n_node_chunk: int, \n", - " input360: bool = True,\n", - " gtype: str = \"grid\",\n", - " ttype: str = \"time\"\n", - ") -> None:\n", - " \"\"\"\n", - " Writes meteorological data from an ERA dataset onto a mesh and saves the result as a zarr file.\n", - "\n", - " :param era_ds: The ERA dataset with the meteorological data.\n", - " :param mesh: The mesh dataset representing the spatial domain.\n", - " :param file_out: The path to the output zarr file.\n", - " :param n_time_chunk: The size of the time chunks for processing.\n", - " :param n_node_chunk: The size of the node chunks for processing.\n", - " :param input360: A flag indicating whether the longitude should be adjusted to a 0-360 range.\n", - " \"\"\"\n", - " # Create the temporary dummy zarr file\n", - " if os.path.exists(file_out):\n", - " shutil.rmtree(file_out)\n", - " x, y, tri = mesh.lon.values, mesh.lat.values, mesh.triface_nodes.values\n", - " nnodes = len(x)\n", - " ntimes = len(era_ds.time)\n", - " zero = da.zeros((ntimes, nnodes), chunks=(n_time_chunk, n_node_chunk))\n", - " \n", - " # Define coordinates and data variables for the output dataset\n", - " coords = {\n", - " 'time': era_ds.time, \n", - " 'node': np.arange(nnodes),\n", - " 'lon': (\"node\", x), \n", - " 'lat': (\"node\", y),\n", - " 'triface_nodes': mesh.triface_nodes,\n", - " }\n", - " data_vars = {}\n", - " for varin in era_ds.data_vars:\n", - " data_vars[varin] = (('time', 'node'), zero)\n", - " xr.Dataset(data_vars=data_vars, coords=coords).to_zarr(file_out, compute=False)\n", - " \n", - " # in the case of \"tstps\"\n", - " if ttype == \"step\":\n", - " t0 = pd.Timestamp(era_ds.time.values)\n", - " seconds = era_ds.step.values / 1e9\n", - " era_ds.time = pd.to_datetime(t0 + pd.Timedelta(seconds=seconds))\n", - "\n", - " # Iterate over nodes in chunks and write data to zarr file\n", - " for ins in range(0, nnodes, n_node_chunk):\n", - " end_node = min(ins + n_node_chunk, nnodes)\n", - " node_chunk = np.arange(ins, end_node)\n", - " mesh_chunk = mesh.isel(node=slice(ins, end_node))\n", - " era_chunk = subset_era_from_mesh(era_ds, mesh_chunk, input360=input360, gtype=gtype)\n", - " \n", - " # Get weights for interpolation\n", - " if gtype == \"grid\":\n", - " nx1d = len(era_chunk.longitude)\n", - " ny1d = len(era_chunk.latitude)\n", - " xx = np.tile(era_chunk.longitude, ny1d).reshape(ny1d, nx1d).T.ravel()\n", - " yy = np.tile(era_chunk.latitude, nx1d)\n", - " else: # for O1280 grids\n", - " xx = era_chunk.longitude\n", - " yy = era_chunk.latitude\n", - " era_chunk = era_chunk.drop_vars([\"number\", \"surface\"]) # useless for meteo exports\n", - " \n", - " in_xy = np.vstack((xx, yy)).T\n", - " if input360:\n", - " in_xy[:, 0][in_xy[:, 0] > 180] -= 360\n", - " out_xy = np.vstack((mesh_chunk.lon, mesh_chunk.lat)).T\n", - " vert, wgts, u_x, g_x = get_weights(in_xy, out_xy) # Assuming get_weights is defined elsewhere\n", - " \n", - " # Interpolate and write data for each variable and time chunk\n", - " for var_name in era_chunk.data_vars:\n", - " for it_chunk in range(0, ntimes, n_time_chunk):\n", - " t_end = min(it_chunk + n_time_chunk, ntimes)\n", - " time_chunk = era_chunk.time[it_chunk:t_end]\n", - " data_chunk = da.zeros((len(time_chunk), len(node_chunk)), chunks=(n_time_chunk, n_node_chunk))\n", - " for it, t_ in enumerate(time_chunk):\n", - " tmp = np.ravel(np.transpose(era_chunk.isel(time=it_chunk+it)[var_name].values))\n", - " data_chunk[it,:] = interp(tmp, vert, wgts, u_x, g_x) # Assuming interp is defined elsewhere\n", - " coords = {'time':time_chunk, 'node': node_chunk}\n", - " ds = xr.Dataset({var_name: (('time', 'node'), data_chunk)}, coords=coords)\n", - " region = {'time': slice(it_chunk, t_end), 'node': slice(ins, end_node)}\n", - " ds.to_zarr(file_out, mode=\"a-\", region=region)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "the script is ready, let's do a sensitivity on the nodes & times " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "let's reduce first the dataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "era_test = era5.isel(time=slice(0,1000))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "times_ = [50,]\n", - "nodes_ = [len(mesh_out.lon)]\n", - "\n", - "time_perf = np.zeros((len(times_), len(nodes_)))\n", - "sizes = np.zeros((len(times_), len(nodes_)))\n", - "for i_t, ttime in enumerate(times_): \n", - " for i_n, nnode in enumerate(nodes_):\n", - " start = time.time()\n", - " write_meteo_on_mesh(era_test, mesh_out, \"era5_2023_uvp.zarr\", ttime, nnode)\n", - " end = time.time()\n", - " time_perf[i_t,i_n] = end - start\n", - " # calculate size\n", - " bytes_per_element = zero.dtype.itemsize\n", - " sizes[i_t,i_n] = ttime * nnode * bytes_per_element / 1024 / 1000\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "visualise the performances" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Convert data for pcolormesh plots into a format compatible with hvplot\n", - "time_perf_mesh_df = pd.DataFrame(time_perf, index=times_, columns=nodes_)\n", - "sizes_mesh_df = pd.DataFrame(sizes, index=times_, columns=nodes_)\n", - "\n", - "options = {\n", - " \"x\":'columns', \n", - " \"y\":'index', \n", - " \"C\":'value', \n", - " \"logx\" : True, \n", - " \"logy\" : True, \n", - " \"colorbar\" : True, \n", - " \"xlabel\" : \"chunk size for nodes\", \n", - " \"ylabel\" : \"chunk size for times\", \n", - " \"width\" : 500,\n", - "}\n", - "\n", - "# Convert data for line plot into a Pandas DataFrame\n", - "sizes_flat = sizes.ravel()\n", - "time_perf_flat = time_perf.ravel()\n", - "idx = np.argsort(sizes_flat)\n", - "line_df = pd.DataFrame({\n", - " 'Size (MB)': sizes_flat[idx],\n", - " 'Time (sec)': time_perf_flat[idx]\n", - "})\n", - "\n", - "# Create hvplot pcolormesh plots\n", - "time_perf_plot = time_perf_mesh_df.hvplot.heatmap(**options).opts(title = \"Time taken for each chunk (in seconds)\")\n", - "sizes_plot = sizes_mesh_df.hvplot.heatmap(**options).opts(title = \"Size of each chunk (in MB)\")\n", - "\n", - "# Create hvplot line plot\n", - "line_plot = line_df.hvplot.line(\n", - " x='Size (MB)', y='Time (sec)', \n", - " title='Time taken vs. Size of each chunk', width = 500)\n", - "\n", - "# Combine plots into a layout\n", - "layout = (time_perf_plot + sizes_plot + line_plot).cols(3)\n", - "\n", - "# Apply some options for better layout\n", - "layout.opts(\n", - " opts.HeatMap(colorbar=True),\n", - " opts.Layout(shared_axes=False, merge_tools=False),\n", - ")\n", - "# # Display the layout\n", - "hv.output(layout)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The best combination seems to be the **max number of nodes with the minimum number of times**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "check if it worked: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ds = xr.open_dataset(\"era5_2023_uvp.zarr\", engine = \"zarr\")\n", - "ds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import thalassa\n", - "import geoviews as gv\n", - "ds = thalassa.normalize(ds)\n", - "# Convert the node data in \"Tabular\" format: https://holoviews.org/user_guide/Tabular_Datasets.html\n", - "df = ds[[\"lon\", \"lat\", \"node\"]].to_dataframe().reset_index()\n", - "# Create a geoviews Points object\n", - "gv_points = gv.Points(df, kdims=[\"lon\", \"lat\"], vdims=[\"node\"]).opts(size = 1, color='k')\n", - "# Create a Dynamic map with the variable you want, e.g. \"depth\"\n", - "raster_dmap = thalassa.plot(ds.sel(time = \"2023-08-27\", method=\"nearest\"), \"u10\")\n", - "# now combine the raster with the points\n", - "combined_dmap = raster_dmap * gv_points\n", - "# plot\n", - "combined_dmap.opts(tools=[\"hover\"], width=1200, height=600)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### finally, convert to Selafin" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def remove(path):\n", - " try:\n", - " if os.path.isfile(path):\n", - " os.remove(path) # Remove a file\n", - " elif os.path.isdir(path):\n", - " if not os.listdir(path): # Check if the directory is empty\n", - " os.rmdir(path)\n", - " else:\n", - " shutil.rmtree(path)\n", - " except OSError as e:\n", - " print(f\"Error: {e.strerror}\")\n", - " \n", - "def write_meteo_selafin(outpath,input_atm_zarr):\n", - " xatm = xr.open_dataset(input_atm_zarr, engine = \"zarr\") \n", - " t0 = pd.Timestamp(ds.time.values[0])\n", - " # Define a mapping from the original variable names to the new ones\n", - " var_map = {\n", - " \"u10\": (\"WINDX\", \"M/S\"),\n", - " \"v10\": (\"WINDY\", \"M/S\"),\n", - " \"msl\": (\"PATM\", \"PASCAL\"),\n", - " \"tmp\": (\"TAIR\", \"DEGREES C\"),\n", - " }\n", - " var_attrs = {}\n", - " for var in ds.data_vars:\n", - " if var in var_map:\n", - " # Attributes for the variable\n", - " var_attrs[var] = (var_map[var][0], var_map[var][1])\n", - " # Add global attributes after concatenation\n", - " xatm.attrs[\"date_start\"] = [t0.year, t0.month, t0.day, t0.hour, t0.minute, t0.second]\n", - " xatm.attrs[\"ikle2\"] = xatm.triface_nodes.values + 1\n", - " xatm.attrs[\"variables\"] = {var: attrs for var, attrs in var_attrs.items()}\n", - " xatm = xatm.rename({\"lon\": \"x\", \"lat\": \"y\"})\n", - " xatm = xatm.drop_vars([\"triface_nodes\"])\n", - " xatm.selafin.write(outpath)\n", - " # remove(input_atm_zarr)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "write_meteo_selafin(\"era5_2023_uvp.slf\", \"era5_2023_uvp.zarr\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "test with O1280 grid: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "O1280_test = xr.open_dataset(\"2023-09-01.uvp.O1280.zarr\", engine = \"zarr\")\n", - "O1280_test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nnode = len(O1280_test.longitude)\n", - "write_meteo_on_mesh(O1280_test, mesh_out, \"01280_v0.zarr\", 50, nnode, input360=True, gtype='tri')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "write_meteo_selafin(\"01280_202309_uvp.slf\", \"01280_v0.zarr\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "it works !! (GIF you see at the beginning of the notebook)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Meteo_Chunks.v0.html b/Meteo_Chunks.v1.html similarity index 97% rename from Meteo_Chunks.v0.html rename to Meteo_Chunks.v1.html index dad0290..abd9339 100644 --- a/Meteo_Chunks.v0.html +++ b/Meteo_Chunks.v1.html @@ -3,7 +3,7 @@ -Meteo_Chunks.v0 +Meteo_Chunks.v1 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1002" + } + }, + "output_type": "display_data" + } + ], + "source": [ + "import dask.array as da\n", + "import numpy as np\n", + "import hvplot.pandas\n", + "import pandas as pd\n", + "import holoviews as hv\n", + "from holoviews import opts\n", + "import pandas as pd\n", + "import xarray as xr\n", + "import shutil\n", + "import os\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Set up parameters for the dataset\n", + "NTIMES = 744 # one month hourly\n", + "NNODES = 100000 # 100k nodes\n", + "chunk_size_time = 10 # Choose an appropriate chunk size for the time dimension" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Array Chunk
Bytes 567.63 MiB 7.63 MiB
Shape (744, 100000) (10, 100000)
Dask graph 75 chunks in 1 graph layer
Data type float64 numpy.ndarray
\n", + "
\n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " 100000\n", + " 744\n", + "\n", + "
" + ], + "text/plain": [ + "dask.array" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero = da.empty((NTIMES, NNODES), chunks=(chunk_size_time, NNODES))\n", + "zero" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "what about if we had a dimension for the variables?" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Array Chunk
Bytes 2.22 GiB 7.63 MiB
Shape (744, 4, 100000) (10, 1, 100000)
Dask graph 300 chunks in 1 graph layer
Data type float64 numpy.ndarray
\n", + "
\n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " 100000\n", + " 4\n", + " 744\n", + "\n", + "
" + ], + "text/plain": [ + "dask.array" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "NVAR = 4\n", + "cube_zero = da.empty((NTIMES,NVAR, NNODES), chunks=(chunk_size_time, 1, NNODES))\n", + "cube_zero" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Delayed('_finalize_store-10e723ec-5be9-4a47-ab99-acbe8dcc6b8f')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Write the data to a temporary Zarr store\n", + "tmp = 'tmp.zarr'\n", + "ds = xr.Dataset({\n", + " \"u10\": (('time', 'node'), zero),\n", + " \"v10\": (('time', 'node'), zero),\n", + " \"msl\": (('time', 'node'), zero),\n", + " \"tmp\": (('time', 'node'), zero),\n", + " \n", + "})\n", + "ds.to_zarr(tmp, compute=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "we generated a metadata file, which will help us to aggregate the data later on. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"metadata\": {\n", + " \".zattrs\": {},\n", + " \".zgroup\": {\n", + " \"zarr_format\": 2\n", + " },\n", + " \"msl/.zarray\": {\n", + " \"chunks\": [\n", + " 10,\n", + " 100000\n", + " ],\n", + " \"compressor\": {\n", + " \"blocksize\": 0,\n", + " \"clevel\": 5,\n", + " \"cname\": \"lz4\",\n", + " \"id\": \"blosc\",\n", + " \"shuffle\": 1\n", + " },\n", + " \"dtype\": \" 2000: \n", + " return np.nan, np.nan\n", + " else: \n", + " ds = xr.Dataset({\n", + " \"u10\": (('time', 'node'), zero),\n", + " \"v10\": (('time', 'node'), zero),\n", + " \"msl\": (('time', 'node'), zero),\n", + " \"tmp\": (('time', 'node'), zero), \n", + " }).to_zarr(tmp, compute=False)\n", + " # Create a Pandas date range for the time coordinates\n", + " time_range = pd.date_range(start=pd.Timestamp(2023, 1, 1), periods=NTIMES, freq='h')\n", + " # Loop over variables\n", + " count = 0\n", + " for var_name in [\"u10\", \"v10\", \"msl\", \"tmp\"]:\n", + " # Loop over time in chunks\n", + " for t_start in range(0, NTIMES, n_time_chunk):\n", + " t_end = min(t_start + n_time_chunk, NTIMES)\n", + " time_chunk = time_range[t_start:t_end]\n", + " # Loop over nodes in chunks\n", + " for i_node in range(0, NNODES, n_node_chunk):\n", + " end_node = min(i_node + n_node_chunk, NNODES)\n", + " data_chunk = np.random.random((t_end - t_start) * (end_node - i_node))\n", + " data_chunk = data_chunk.reshape((t_end - t_start, end_node - i_node))\n", + " node_chunk = np.arange(i_node, end_node)\n", + " coords = {'node': node_chunk, 'time':time_chunk}\n", + " ds = xr.Dataset({var_name: (('time', 'node'), data_chunk)}, coords=coords)\n", + " region = {'time': slice(t_start, t_end), 'node': slice(i_node, end_node)}\n", + " ds.to_zarr(tmp, mode=\"a-\", region=region)\n", + " count += 1\n", + " \n", + " end = time.time()\n", + " print(f\"Chunk size: times:{n_time_chunk} x nodes:{n_node_chunk} - {chunk_size_mb}kB\")\n", + " return end - start, chunk_size_mb" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Iterate for a range of chunk sizes" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Chunk size: times:10 x nodes:5000 - 0.390625kB\n", + "Chunk size: times:10 x nodes:10000 - 0.78125kB\n", + "Chunk size: times:10 x nodes:50000 - 3.90625kB\n", + "Chunk size: times:10 x nodes:100000 - 7.8125kB\n", + "Chunk size: times:50 x nodes:5000 - 1.953125kB\n", + "Chunk size: times:50 x nodes:10000 - 3.90625kB\n", + "Chunk size: times:50 x nodes:50000 - 19.53125kB\n", + "Chunk size: times:50 x nodes:100000 - 39.0625kB\n", + "Chunk size: times:100 x nodes:5000 - 3.90625kB\n", + "Chunk size: times:100 x nodes:10000 - 7.8125kB\n", + "Chunk size: times:100 x nodes:50000 - 39.0625kB\n", + "Chunk size: times:100 x nodes:100000 - 78.125kB\n", + "Chunk size: times:150 x nodes:5000 - 5.859375kB\n", + "Chunk size: times:150 x nodes:10000 - 11.71875kB\n", + "Chunk size: times:150 x nodes:50000 - 58.59375kB\n", + "Chunk size: times:150 x nodes:100000 - 117.1875kB\n", + "Chunk size: times:200 x nodes:5000 - 7.8125kB\n", + "Chunk size: times:200 x nodes:10000 - 15.625kB\n", + "Chunk size: times:200 x nodes:50000 - 78.125kB\n", + "Chunk size: times:200 x nodes:100000 - 156.25kB\n", + "Chunk size: times:250 x nodes:5000 - 9.765625kB\n", + "Chunk size: times:250 x nodes:10000 - 19.53125kB\n", + "Chunk size: times:250 x nodes:50000 - 97.65625kB\n", + "Chunk size: times:250 x nodes:100000 - 195.3125kB\n", + "Chunk size: times:300 x nodes:5000 - 11.71875kB\n", + "Chunk size: times:300 x nodes:10000 - 23.4375kB\n", + "Chunk size: times:300 x nodes:50000 - 117.1875kB\n", + "Chunk size: times:300 x nodes:100000 - 234.375kB\n", + "Chunk size: times:350 x nodes:5000 - 13.671875kB\n", + "Chunk size: times:350 x nodes:10000 - 27.34375kB\n", + "Chunk size: times:350 x nodes:50000 - 136.71875kB\n", + "Chunk size: times:350 x nodes:100000 - 273.4375kB\n", + "Chunk size: times:400 x nodes:5000 - 15.625kB\n", + "Chunk size: times:400 x nodes:10000 - 31.25kB\n", + "Chunk size: times:400 x nodes:50000 - 156.25kB\n", + "Chunk size: times:400 x nodes:100000 - 312.5kB\n", + "Chunk size: times:500 x nodes:5000 - 19.53125kB\n", + "Chunk size: times:500 x nodes:10000 - 39.0625kB\n", + "Chunk size: times:500 x nodes:50000 - 195.3125kB\n", + "Chunk size: times:500 x nodes:100000 - 390.625kB\n" + ] + } + ], + "source": [ + "chunksizes = [10, 50, 100, 150, 200, 250, 300, 350, 400, 500]\n", + "nodessizes = [5000, 10000, 50000, 100000]#, 500000, 1000000]\n", + "\n", + "time_perf = np.zeros((len(chunksizes), len(nodessizes)))\n", + "sizes = np.zeros((len(chunksizes), len(nodessizes)))\n", + "for i_t, chunktime in enumerate(chunksizes): \n", + " for i_n, chunknode in enumerate(nodessizes):\n", + " perf, size = test_chunk_size(chunktime, chunknode)\n", + " time_perf[i_t,i_n] = perf\n", + " sizes[i_t,i_n] = size" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Layout\n", + " .HeatMap.I :HeatMap [columns,index] (value)\n", + " .HeatMap.II :HeatMap [columns,index] (value)\n", + " .Curve.I :Curve [Size (MB)] (Time (sec))" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1004" + } + }, + "output_type": "display_data" + } + ], + "source": [ + "# Convert data for pcolormesh plots into a format compatible with hvplot\n", + "time_perf_mesh_df = pd.DataFrame(time_perf, index=chunksizes, columns=nodessizes)\n", + "sizes_mesh_df = pd.DataFrame(sizes, index=chunksizes, columns=nodessizes)\n", + "\n", + "# Convert data for line plot into a Pandas DataFrame\n", + "sizes_flat = sizes.ravel()\n", + "time_perf_flat = time_perf.ravel()\n", + "idx = np.argsort(sizes_flat)\n", + "line_df = pd.DataFrame({\n", + " 'Size (MB)': sizes_flat[idx],\n", + " 'Time (sec)': time_perf_flat[idx]\n", + "})\n", + "\n", + "options = {\n", + " \"x\":'columns', \n", + " \"y\":'index', \n", + " \"C\":'value', \n", + " \"logx\":True, \n", + " \"colorbar\":True, \n", + " \"xlabel\":\"chunk size for nodes\", \n", + " \"ylabel\" : \"chunk size for times\", \n", + " \"width\" : 500,\n", + "}\n", + "\n", + "# Create hvplot pcolormesh plots\n", + "time_perf_plot = time_perf_mesh_df.hvplot.heatmap(title = 'Time taken for the export (in seconds)', **options)\n", + "sizes_plot = sizes_mesh_df.hvplot.heatmap(title='Size of each chunk (in MB)', **options)\n", + "\n", + "# Create hvplot line plot\n", + "line_plot = line_df.hvplot.line(\n", + " x='Size (MB)', y='Time (sec)', \n", + " title='Time taken vs. Size of each chunk', width = 500)\n", + "\n", + "# Combine plots into a layout\n", + "layout = (time_perf_plot + sizes_plot + line_plot).cols(3)\n", + "\n", + "# Apply some options for better layout\n", + "layout.opts(\n", + " opts.HeatMap(colorbar=True),\n", + " opts.Layout(shared_axes=False, merge_tools=False),\n", + ")\n", + "# # Display the layout\n", + "hv.output(layout)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Conclusion: that the bigger the chunk size is, the smaller is the time to process the data. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 - test with real data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We used previously the `region` parameter to append chunks to the zarr file.\n", + "\n", + "According to the [docs](https://docs.xarray.dev/en/stable/user-guide/io.html#modifying-existing-zarr-stores), \n", + "> region (`dict` or `\"auto\"`, optional) – Optional mapping from dimension names to integer slices along dataset dimensions to indicate the region of existing zarr array(s) in which to write this dataset’s data. For example, `{'x': slice(0, 1000), 'y': slice(10000, 11000)}` would indicate that values should be written to the region `0:1000` along `x` and `10000:11000` along `y`.\n", + "\n", + "**Conclusion**: We can only enter **monotonic** slices in the `region` dictionary argument. That means we need specify the output chunks with monotomic slices of a **constant size**.\n", + "\n", + "To have coherent geographic we need to reorder the nodes along their coordinates" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 load mesh file\n", + "we will use the `xarray-selafin` package to load the mesh file" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The following packages are already present in the pyproject.toml and will be skipped:\n", + "\n", + " - \u001b[36mxarray-selafin\u001b[39m\n", + "\n", + "If you want to update it to the latest compatible version, you can use `poetry update package`.\n", + "If you prefer to upgrade it to the latest available version, you can use `poetry add package@latest`.\n", + "\n", + "Nothing to add.\n" + ] + } + ], + "source": [ + "! poetry add xarray-selafin" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 1MB\n",
+       "Dimensions:  (time: 1, node: 76064)\n",
+       "Coordinates:\n",
+       "    x        (node) float32 304kB ...\n",
+       "    y        (node) float32 304kB ...\n",
+       "  * time     (time) datetime64[ns] 8B 2024-04-12T12:35:12\n",
+       "Dimensions without coordinates: node\n",
+       "Data variables:\n",
+       "    B        (time, node) float32 304kB ...\n",
+       "    W        (time, node) float32 304kB ...\n",
+       "Attributes:\n",
+       "    title:       Converted with array-serafin\n",
+       "    language:    en\n",
+       "    float_size:  4\n",
+       "    endian:      >\n",
+       "    params:      (1, 0, 0, 0, 0, 0, 0, 147503, 0, 1)\n",
+       "    ipobo:       [ 0  0  0 ...  0 87  0]\n",
+       "    ikle2:       [[67074 65537 69504]\\n [  257 27922 31606]\\n [27922   257 64...\n",
+       "    variables:   {'B': ('BOTTOM', 'M'), 'W': ('BOTTOM FRICTION', '')}\n",
+       "    date_start:  (1900, 1, 1, 0, 0, 0)
" + ], + "text/plain": [ + " Size: 1MB\n", + "Dimensions: (time: 1, node: 76064)\n", + "Coordinates:\n", + " x (node) float32 304kB ...\n", + " y (node) float32 304kB ...\n", + " * time (time) datetime64[ns] 8B 2024-04-12T12:35:12\n", + "Dimensions without coordinates: node\n", + "Data variables:\n", + " B (time, node) float32 304kB ...\n", + " W (time, node) float32 304kB ...\n", + "Attributes:\n", + " title: Converted with array-serafin\n", + " language: en\n", + " float_size: 4\n", + " endian: >\n", + " params: (1, 0, 0, 0, 0, 0, 0, 147503, 0, 1)\n", + " ipobo: [ 0 0 0 ... 0 87 0]\n", + " ikle2: [[67074 65537 69504]\\n [ 257 27922 31606]\\n [27922 257 64...\n", + " variables: {'B': ('BOTTOM', 'M'), 'W': ('BOTTOM FRICTION', '')}\n", + " date_start: (1900, 1, 1, 0, 0, 0)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# load mesh file\n", + "mesh = xr.open_dataset(\"global-v0.slf\", engine = 'selafin')\n", + "mesh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 split and reorder along regions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "let's visualise the mesh nodes number along the lat/lon dimensions. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Points [x,y] (id)" + ] + }, + "execution_count": 13, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1188" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "def plot_mesh(x, y): \n", + " df = pd.DataFrame({'x': x, 'y': y, 'id': np.arange(len(x))})\n", + " im = df.hvplot.points(x='x', y='y', c='id',s=10)\n", + " return im\n", + "\n", + "x, y = mesh.x.values, mesh.y.values\n", + "plot_mesh(x,y).opts(width = 1200, height = 600,cmap='tab20c')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "out of the mesher, mesh nodes are not sorted, we need to reorder them.\n", + "\n", + "The methodology will proceed as following : \n", + "\n", + "1. Normalize the longitude (x) and latitude (y) coordinates of the nodes.\n", + "2. Compute the ordering weights for the nodes.\n", + "3. For each region, determine which nodes fall within that region.\n", + "4. Reorder the nodes within each region based on the computed weights.\n", + "5. Remap the connectivity of the triangles to reflect the new node ordering." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "first we'll load the regions from a `json` file we previously created on `QGIS`" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'type': 'FeatureCollection',\n", + " 'name': 'world_regions',\n", + " 'crs': {'type': 'name',\n", + " 'properties': {'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}},\n", + " 'features': [{'type': 'Feature',\n", + " 'properties': {'id': 0, 'name': 'n_america'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-180.0, 17.5],\n", + " [-180.0, 90.0],\n", + " [-100.0, 90.0],\n", + " [-100.0, 17.5],\n", + " [-180.0, 17.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 1, 'name': 'c_america1'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-180.0, 15.5],\n", + " [-180.0, 17.5],\n", + " [-93.0, 17.5],\n", + " [-93.0, 15.5],\n", + " [-180.0, 15.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 2, 'name': 'c_america2'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-180.0, 10.5],\n", + " [-180.0, 15.5],\n", + " [-84.5, 15.5],\n", + " [-84.5, 10.5],\n", + " [-180.0, 10.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 3, 'name': 'c_america3'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-180.0, 9.0],\n", + " [-180.0, 10.5],\n", + " [-83.5, 10.5],\n", + " [-83.5, 9.0],\n", + " [-180.0, 9.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 4, 'name': 's_america'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-180.0, -90.0],\n", + " [-180.0, 9.0],\n", + " [-70.0, 9.0],\n", + " [-70.0, -90.0],\n", + " [-180.0, -90.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 5, 'name': 'n_atlantic'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-100.0, 30.5],\n", + " [-100.0, 90.0],\n", + " [-32.5, 90.0],\n", + " [-32.5, 30.5],\n", + " [-100.0, 30.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 6, 'name': 'caribbean1'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-100.0, 17.5],\n", + " [-100.0, 30.5],\n", + " [-32.5, 30.5],\n", + " [-32.5, 17.5],\n", + " [-100.0, 17.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 7, 'name': 'carribean2'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-93.0, 15.5],\n", + " [-93.0, 17.5],\n", + " [-32.5, 17.5],\n", + " [-32.5, 15.5],\n", + " [-93.0, 15.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 8, 'name': 'caribbean3'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-84.5, 10.5],\n", + " [-84.5, 15.5],\n", + " [-32.5, 15.5],\n", + " [-32.5, 10.5],\n", + " [-84.5, 10.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 9, 'name': 'caribbean4'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-83.5, 9.0],\n", + " [-83.5, 10.5],\n", + " [-32.5, 10.5],\n", + " [-32.5, 9.0],\n", + " [-83.5, 9.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 10, 'name': 'n_atlantic'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-32.5, 48.0],\n", + " [-32.5, 90.0],\n", + " [46.0, 90.0],\n", + " [46.0, 48.0],\n", + " [-32.5, 48.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 11, 'name': 'gascogne'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-32.5, 41.5],\n", + " [-32.5, 48.0],\n", + " [-0.0, 48.0],\n", + " [0.0, 41.5],\n", + " [-32.5, 41.5]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 12, 'name': 'c_atlantic'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-32.5, 30.0],\n", + " [-32.5, 41.5],\n", + " [-6.0, 41.5],\n", + " [-6.0, 30.0],\n", + " [-32.5, 30.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 13, 'name': 'med1'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-6.0, 30.0],\n", + " [-6.0, 41.5],\n", + " [0.0, 41.5],\n", + " [0.0, 30.0],\n", + " [-6.0, 30.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 14, 'name': 'med2'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[0.0, 30.0],\n", + " [-0.0, 48.0],\n", + " [46.0, 48.0],\n", + " [46.0, 30.0],\n", + " [0.0, 30.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 15, 'name': 'africa'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-32.5, 9.0],\n", + " [-32.5, 30.0],\n", + " [25.0, 30.0],\n", + " [25.0, 9.0],\n", + " [-32.5, 9.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 16, 'name': 's_atlantic'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[-70.0, -90.0],\n", + " [-70.0, 9.0],\n", + " [25.0, 9.0],\n", + " [25.0, -90.0],\n", + " [-70.0, -90.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 17, 'name': 'red_sea'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[25.0, 9.0],\n", + " [25.0, 30.0],\n", + " [46.0, 30.0],\n", + " [46.0, 9.0],\n", + " [25.0, 9.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 18, 'name': 'arabic'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[46.0, 9.0],\n", + " [46.0, 48.0],\n", + " [58.0, 48.0],\n", + " [58.0, 9.0],\n", + " [46.0, 9.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 19, 'name': 'e_africa'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[25.0, -90.0],\n", + " [25.0, 9.0],\n", + " [58.0, 9.0],\n", + " [58.0, -90.0],\n", + " [25.0, -90.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 20, 'name': 'russia'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[46.0, 48.0],\n", + " [46.0, 90.0],\n", + " [180.0, 90.0],\n", + " [180.0, 48.0],\n", + " [46.0, 48.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 21, 'name': 's_indian'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[58.0, -90.0],\n", + " [58.0, 48.0],\n", + " [99.0, 48.0],\n", + " [99.0, -90.0],\n", + " [58.0, -90.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 22, 'name': 'se_asia'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[99.0, -11.0],\n", + " [99.0, 48.0],\n", + " [180.0, 48.0],\n", + " [180.0, -11.0],\n", + " [99.0, -11.0]]]]}},\n", + " {'type': 'Feature',\n", + " 'properties': {'id': 23, 'name': 'australia'},\n", + " 'geometry': {'type': 'MultiPolygon',\n", + " 'coordinates': [[[[99.0, -90.0],\n", + " [99.0, -11.0],\n", + " [180.0, -11.0],\n", + " [180.0, -90.0],\n", + " [99.0, -90.0]]]]}}]}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "with open(\"world_regions.json\") as f:\n", + " json_regions = json.load(f)\n", + "json_regions" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_76825/4051364510.py:3: FutureWarning: The geopandas.dataset module is deprecated and will be removed in GeoPandas 1.0. You can get the original 'naturalearth_lowres' data from https://www.naturalearthdata.com/downloads/110m-cultural-vectors/.\n", + " countries = gpd.read_file(gpd.datasets.get_path(\"naturalearth_lowres\"))\n" + ] + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Overlay\n", + " .Polygons.I :Polygons [x,y]\n", + " .Polygons.II :Polygons [x,y] (id,name)" + ] + }, + "execution_count": 15, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1265" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "import geopandas as gpd\n", + "import hvplot.pandas # This import is necessary to use the hvplot accessor\n", + "countries = gpd.read_file(gpd.datasets.get_path(\"naturalearth_lowres\"))\n", + "world_regions = gpd.read_file(\"world_regions.json\")\n", + "countries_plot = countries.hvplot(color='k')\n", + "world_regions_plot = world_regions.hvplot(\n", + " alpha=0.6, \n", + " c='id', \n", + " cmap='tab20c',\n", + " hover_cols=['id','name'],)\n", + "overlay = countries_plot * world_regions_plot\n", + "overlay.opts(\n", + " width = 1200,\n", + " height = 600,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3 functions below: \n", + "1. `reorder_nodes_within_region`: reorder the nodes within a given region based on the computed weights.\n", + "2. `remap_connectivity`: remap the connectivity of the triangles to reflect the new node ordering.\n", + "3. `reorder_mesh`: main functions that translates input mesh to \"ordered\" mesh. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Tuple, List, Iterable\n", + "import numpy_indexed as npi\n", + "\n", + "def remap_connectivity(\n", + " tri: np.ndarray, \n", + " mapping: np.ndarray\n", + " ) -> np.ndarray:\n", + " \"\"\"Remap the connectivity of a triangular mesh based on the new node order.\n", + "\n", + " Args:\n", + " tri: The original connectivity array of the triangular mesh.\n", + " mapping: The array that maps old node indices to new ones.\n", + "\n", + " Returns:\n", + " The remapped connectivity array for the triangular mesh.\n", + " \"\"\" \n", + " remapped_nodes = np.arange(len(mapping))\n", + " remapped_triface_nodes = np.c_[\n", + " npi.remap(tri[:, 0], mapping, remapped_nodes),\n", + " npi.remap(tri[:, 1], mapping, remapped_nodes),\n", + " npi.remap(tri[:, 2], mapping, remapped_nodes),\n", + " ]\n", + " return remapped_triface_nodes\n", + "\n", + "\n", + "def reorder_nodes_within_region(\n", + " x: np.ndarray, \n", + " y: np.ndarray, \n", + " region_polygon: gpd.GeoDataFrame, \n", + " order_wgts: np.ndarray\n", + " ) -> np.ndarray:\n", + " \"\"\"Reorder nodes within a given region based on their weights.\n", + "\n", + " Args:\n", + " x: The x-coordinates of the nodes.\n", + " y: The y-coordinates of the nodes.\n", + " region_polygon: The polygon representing the region.\n", + " order_wgts: The weights for ordering the nodes.\n", + "\n", + " Returns:\n", + " The indices of the reordered nodes within the given region.\n", + " \"\"\" \n", + " bbox = region_polygon.bounds\n", + " points_in_region = (y >= bbox[1]) & (y <= bbox[3]) & (x >= bbox[0]) & (x <= bbox[2])\n", + " indices_in_region = np.where(points_in_region)[0]\n", + " order_wgts_in_region = order_wgts[indices_in_region]\n", + " idx_sort = np.argsort(order_wgts_in_region)\n", + " mapping = np.arange(len(x))\n", + " mapping[indices_in_region] = indices_in_region[idx_sort]\n", + " return indices_in_region[idx_sort]\n", + "\n", + "\n", + "def reorder_mesh(\n", + " x: np.ndarray, \n", + " y: np.ndarray, \n", + " tri:np.ndarray, \n", + " regions: gpd.GeoDataFrame\n", + " ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[int]]:\n", + " \"\"\"Reorder the mesh nodes and remap the connectivity for each region.\n", + "\n", + " Args:\n", + " mesh: The dataset representing the mesh.\n", + " regions: A GeoDataFrame representing the regions.\n", + "\n", + " Returns:\n", + " A tuple containing the reordered x-coordinates, y-coordinates, \n", + " remapped connectivity, and the global sorting indices.\n", + " \"\"\" # 1 normalise\n", + " normalized_lon = mesh.x - np.min(mesh.x)\n", + " normalized_lat = mesh.y - np.min(mesh.y)\n", + " # 2 compute ordering\n", + " order_wgts = (normalized_lon) + (180-normalized_lat) * 360\n", + " # 3 test point in regions and fill in mapping / sorted indices\n", + " global_sorted = []\n", + " for ir, region in regions.iterrows():\n", + " region_polygon = region['geometry']\n", + " # 4. Reorder the nodes within each region \n", + " sorted_indices = reorder_nodes_within_region(x,y, region_polygon, order_wgts)\n", + " global_sorted.extend(sorted_indices)\n", + " # 5. Remap the connectivity \n", + " tri_out = remap_connectivity(tri, np.array(global_sorted))\n", + " return x[global_sorted], y[global_sorted], tri_out, global_sorted\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "x, y, tri = mesh.x.values, mesh.y.values, mesh.attrs['ikle2'] - 1\n", + "x_, y_, tri_, map_ = reorder_mesh(x, y, tri, world_regions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "test if the renumbering worked as expected: " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Points [x,y] (id)" + ] + }, + "execution_count": 18, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1975" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "plot_mesh(x_, y_).opts(width = 1200, height = 600, cmap = 'tab20c')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "test if the triangle remapping worked as expected: " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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0DnnnJP49+mnn676+nqdeOKJ+o//+A++MAUi7OKLL078+7TTTtPpp5+uk046SU899ZS++MUv+tiycCFwB9A111yjGTNm5Nxm6NChRe+/vr5e3/ve99TV1aWKigrV1tYeNltpe3u7Kisr+R+1T5y8Bmpraw+bnTj+ftfW1ib+yzVgvlKui/r6en300Uf605/+pGHDhmV9z6VD1wWC6dhjj9URRxyR8f3lvY2G/v376xOf+ITeeOMN/b//9/+0b98+7d69O6XKzfUQbvH3tr29Xccdd1zi/vb2do0aNSqxTfpEih999JH+9re/cW2E1NChQ3XsscfqjTfe0Be/+EWuAYcQuANo4MCBGjhwoGv737ZtmwYMGJCocjU0NOhXv/pVyjZr165VQ0ODa21Abk5eAw0NDbr11lu1a9euRLehtWvXqrKyUiNGjEhswzVgvlKui23btqmsrCxxDTQ0NOj666/X/v371bNnT0kH3/Nhw4bRnTzgysvLNWbMGK1bty6xIkV3d7fWrVun5uZmfxsHT/z973/XH//4R1122WUaM2aMevbsqXXr1umiiy6SJO3YsUM7d+7kd3yIDRkyRLW1tVq3bl0iYMdiMW3atCmxqklDQ4N2796tLVu2aMyYMZKk9evXq7u7W/X19X41HS76v//7P/31r39NfAnDNeAQv2dtg7vefvtt6/nnn7cWLlxo9evXz3r++eet559/3tqzZ49lWZb1P//zP9bSpUutl156yXr99det++67z+rTp4+1YMGCxD7efPNNq0+fPta3vvUta/v27da9995rHXHEEdYTTzzh12GhAPmugY8++sj65Cc/aU2cONHatm2b9cQTT1gDBw605s+fn9gH10C4PPPMM9bdd99tbdu2zfrjH/9orVixwho4cKD1la98JbHN7t27rZqaGuuyyy6zXn75ZevnP/+51adPH+tHP/qRjy2HU37+859bFRUV1kMPPWS9+uqr1hVXXGH1798/ZbUChMc111xjPfXUU9Zbb71lPf3009aECROsY4891tq1a5dlWZb1ta99zTrhhBOs9evXW5s3b7YaGhqshoYGn1uNUu3Zsyfx/3xJ1pIlS6znn3/eevvtty3Lsqzbb7/d6t+/v/Xoo49aL774ojV58mRryJAh1ocffpjYx9lnn22deeaZ1qZNm6zf//731imnnGL967/+q1+HhALlugb27NljXXvttVZra6v11ltvWb/5zW+s0aNHW6eccoq1d+/exD64BkpH4A656dOnW5IOuz355JOWZR2c2n/UqFFWv379rL59+1pnnHGG9cADD1gHDhxI2c+TTz5pjRo1yiovL7eGDh1qLV++3PuDQVHyXQOWZVl/+tOfrHPOOcfq3bu3deyxx1rXXHONtX///pT9cA2Ex5YtW6z6+nqrqqrK6tWrlzV8+HDrtttuS/kfrGVZ1gsvvGB99rOftSoqKqyPfexj1u233+5Ti+GGH/7wh9YJJ5xglZeXW5/+9KetjRs3+t0kuOTLX/6yddxxx1nl5eXWxz72MevLX/6y9cYbbyQe//DDD60rr7zSGjBggNWnTx9rypQp1vvvv+9ji+GEJ598MuP//6dPn25Z1sGlwW688UarpqbGqqiosL74xS9aO3bsSNnHX//6V+tf//VfrX79+lmVlZXWzJkzE1/Yw3y5roF//OMf1sSJE62BAwdaPXv2tE488USrqanpsC9euQZK18OyktZ/AgAAAAAAjmAdbgAAAAAAXEDgBgAAAADABQRuAAAAAABcQOAGAAAAAMAFBG4AAAAAAFxA4AYAAAAAwAUEbgAAAAAAXEDgBgAAAADABQRuAAAAAABcQOAGAAAAAMAFBG4AAAAAAFzw/wGQuOPDhJq5pQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "fig, ax = plt.subplots(1,1, figsize = (12,6))\n", + "def is_overlapping(tris, meshx):\n", + " PIR = 180\n", + " x1, x2, x3 = meshx[tris].T\n", + " return np.logical_or(abs(x2 - x1) > PIR, abs(x3 - x1) > PIR, abs(x3 - x2) > PIR)\n", + "m = is_overlapping(tri_ ,x_)\n", + "ax.tricontourf(x_, y_, tri_[~m], np.arange(len(x)),cmap = 'tab20c')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### finalise and save the new mesh dataset: " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "using [thalassa](https://github.com/ec-jrc/Thalassa/blob/b9d977cd6999e73f5ad884e9e7b96d4041b60827/thalassa/normalization.py#L27)'s `GENERIC` Format" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 3MB\n",
+       "Dimensions:        (node: 76064, triface: 147503, three: 3)\n",
+       "Dimensions without coordinates: node, triface, three\n",
+       "Data variables:\n",
+       "    lon            (node) float32 304kB -180.0 -119.2 -151.0 ... 172.1 177.9\n",
+       "    lat            (node) float32 304kB 90.0 89.45 88.96 ... -83.67 -84.07\n",
+       "    triface_nodes  (triface, three) int32 2MB 28287 28286 28196 ... 8449 8594\n",
+       "    depth          (node) float32 304kB 4.228e+03 2.633e+03 ... 107.0 254.0
" + ], + "text/plain": [ + " Size: 3MB\n", + "Dimensions: (node: 76064, triface: 147503, three: 3)\n", + "Dimensions without coordinates: node, triface, three\n", + "Data variables:\n", + " lon (node) float32 304kB -180.0 -119.2 -151.0 ... 172.1 177.9\n", + " lat (node) float32 304kB 90.0 89.45 88.96 ... -83.67 -84.07\n", + " triface_nodes (triface, three) int32 2MB 28287 28286 28196 ... 8449 8594\n", + " depth (node) float32 304kB 4.228e+03 2.633e+03 ... 107.0 254.0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mesh_out = xr.Dataset({\n", + " 'lon': (['node'], x_),\n", + " 'lat': (['node'], y_),\n", + " 'triface_nodes': (['triface', 'three'], tri_),\n", + " 'depth': (['node'], mesh.B.isel(time=0).values[map_]),\n", + "})\n", + "mesh_out" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "check the depth assignation" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "fig, ax = plt.subplots(1,1, figsize = (12,6))\n", + "ax.tricontourf(x_, y_, tri_[~m], mesh_out.depth)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### export back to selafin" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "renumbered_mesh = xr.Dataset({\n", + " \"B\": ((\"time\", \"node\"), [mesh_out.depth.values]),\n", + " },\n", + " coords={\n", + " \"x\": (['node'], x_),\n", + " \"y\": (['node'], y_),\n", + " \"time\": [pd.Timestamp.now()],\n", + "})\n", + "renumbered_mesh.attrs[\"variables\"] = {\"B\": (\"BOTTOM\", \"M\") }\n", + "renumbered_mesh.attrs['ikle2'] = tri_ + 1\n", + "renumbered_mesh.selafin.write(\"global-v0.renumbered.slf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 interpolate meteo and append on to zarr file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "we will load ERA5 reanalysis data and test the chunking size on the `time` and `node` dimensions: \n", + " * for the `time` dimension, the chunking is quite straightforward.\n", + " * for the `node` dimension, the chunking gets more complex because we deal with 2 different meshes. we will explain why in details below" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 218GB\n",
+       "Dimensions:    (longitude: 1440, latitude: 721, time: 8760)\n",
+       "Coordinates:\n",
+       "  * longitude  (longitude) float32 6kB 0.0 0.25 0.5 0.75 ... 359.2 359.5 359.8\n",
+       "  * latitude   (latitude) float32 3kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0\n",
+       "  * time       (time) datetime64[ns] 70kB 2023-01-01 ... 2023-12-31T23:00:00\n",
+       "Data variables:\n",
+       "    msl        (time, latitude, longitude) float64 73GB ...\n",
+       "    u10        (time, latitude, longitude) float64 73GB ...\n",
+       "    v10        (time, latitude, longitude) float64 73GB ...\n",
+       "Attributes:\n",
+       "    Conventions:  CF-1.6\n",
+       "    history:      2024-04-04 09:42:07 GMT by grib_to_netcdf-2.25.1: /opt/ecmw...
" + ], + "text/plain": [ + " Size: 218GB\n", + "Dimensions: (longitude: 1440, latitude: 721, time: 8760)\n", + "Coordinates:\n", + " * longitude (longitude) float32 6kB 0.0 0.25 0.5 0.75 ... 359.2 359.5 359.8\n", + " * latitude (latitude) float32 3kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0\n", + " * time (time) datetime64[ns] 70kB 2023-01-01 ... 2023-12-31T23:00:00\n", + "Data variables:\n", + " msl (time, latitude, longitude) float64 73GB ...\n", + " u10 (time, latitude, longitude) float64 73GB ...\n", + " v10 (time, latitude, longitude) float64 73GB ...\n", + "Attributes:\n", + " Conventions: CF-1.6\n", + " history: 2024-04-04 09:42:07 GMT by grib_to_netcdf-2.25.1: /opt/ecmw..." + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# load ERA5 reanalysis data\n", + "era5 = xr.open_dataset(\"era5_2023_uvp.nc\")\n", + "era5" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "first we will the interpolation functions used in TELEMAC ( [`optim_gen-atm`](https://gitlab.pam-retd.fr/otm/telemac-mascaret/-/blob/optim-gen-atm/scripts/python3/utils/geometry.py) branch)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.spatial import Delaunay, cKDTree\n", + "def get_weights(in_xy, out_xy, d=2):\n", + " t = Delaunay(in_xy) # triangulate output mesh\n", + " s = t.find_simplex(out_xy) \n", + " vert = np.take(t.simplices, np.maximum(s, 0), axis=0) # Use max to avoid negative indices\n", + " t_ = np.take(t.transform, np.maximum(s, 0), axis=0)\n", + " delta = out_xy - t_[:, d]\n", + " bary = np.einsum('njk,nk->nj', t_[:, :d, :], delta)\n", + " wgts = np.hstack((bary, 1 - bary.sum(axis=1, keepdims=True)))\n", + " # Points outside the out_xy\n", + " out_idx_out = s < 0 \n", + " if np.any(out_idx_out):\n", + " # For points outside, find nearest neighbors\n", + " tree = cKDTree(in_xy)\n", + " _, in_idx_out = tree.query(out_xy[out_idx_out])\n", + " else : \n", + " in_idx_out = None\n", + " return vert, wgts, out_idx_out, in_idx_out\n", + "\n", + "\n", + "def interp(values, vtx, wts, out_idx_out, in_idx_out):\n", + " res = np.einsum('nj,nj->n', np.take(values, vtx), wts)\n", + " if in_idx_out is not None:\n", + " res[out_idx_out] = values[in_idx_out]\n", + " return res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "here we will mimic the first function from above in order to append to a zarr store" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "# Function to subset ERA data based on the mesh extent\n", + "def subset_era_from_mesh(\n", + " era: xr.Dataset,\n", + " mesh: xr.Dataset,\n", + " input360: bool,\n", + " gtype: str,\n", + ") -> xr.Dataset:\n", + " \"\"\"\n", + " Selects a subset of ERA data that overlaps with the provided mesh's geographical extent.\n", + "\n", + " :param era: The ERA dataset from which to select a subset.\n", + " :param mesh: The mesh dataset defining the geographical extent for the subset selection.\n", + " :param input360: A flag indicating whether the longitude should be adjusted to a 0-360 range.\n", + " :return: A subset of the ERA dataset that falls within the mesh's geographical extent.\n", + " \"\"\"\n", + " xmin, xmax, ymin, ymax = mesh.x.min(), mesh.x.max(), mesh.y.min(), mesh.y.max()\n", + " if input360:\n", + " xmin, xmax = np.mod(xmin + 360, 360), np.mod(xmax + 360, 360)\n", + " if xmax < xmin:\n", + " xmin, xmax = 0, 360\n", + " if gtype == \"grid\":\n", + " era_chunk = era.sel(longitude=slice(xmin, xmax), latitude=slice(ymax, ymin))\n", + " else: # for 01280 grid\n", + " mask_lon = (era.longitude >= xmin) & (era.longitude <= xmax)\n", + " mask_lat = (era.latitude >= ymin) & (era.latitude <= ymax)\n", + " mask = mask_lon & mask_lat\n", + " indices = np.where(mask)[0]\n", + " era_chunk = era.isel(values=indices)\n", + " return era_chunk\n", + "\n", + "\n", + "# Function to write meteorological data onto a mesh\n", + "def write_meteo_on_mesh(\n", + " era_ds: xr.Dataset,\n", + " mesh: xr.Dataset,\n", + " file_out: str,\n", + " n_time_chunk: int,\n", + " n_node_chunk: int,\n", + " input360: bool = True,\n", + " gtype: str = \"grid\",\n", + " ttype: str = \"time\",\n", + ") -> None:\n", + " \"\"\"\n", + " Writes meteorological data from an ERA dataset onto a mesh and saves the result as a zarr file.\n", + "\n", + " :param era_ds: The ERA dataset with the meteorological data.\n", + " :param mesh: The mesh dataset representing the spatial domain.\n", + " :param file_out: The path to the output zarr file.\n", + " :param n_time_chunk: The size of the time chunks for processing.\n", + " :param n_node_chunk: The size of the node chunks for processing.\n", + " :param input360: A flag indicating whether the longitude should be adjusted to a 0-360 range.\n", + " \"\"\"\n", + " # Create the temporary dummy zarr file\n", + " if os.path.exists(file_out):\n", + " shutil.rmtree(file_out)\n", + " x, y, tri = mesh.x.values, mesh.y.values, mesh.attrs[\"ikle2\"] - 1\n", + " nnodes = len(x)\n", + " ntimes = len(era_ds.time)\n", + " zero = da.zeros((ntimes, nnodes), chunks=(n_time_chunk, n_node_chunk))\n", + "\n", + " # Define coordinates and data variables for the output dataset\n", + " coords = {\n", + " \"time\": era_ds.time,\n", + " \"node\": np.arange(nnodes),\n", + " \"lon\": (\"node\", x),\n", + " \"lat\": (\"node\", y),\n", + " \"triface_nodes\": ((\"triface\", \"three\"), tri),\n", + " }\n", + " data_vars = {}\n", + " for varin in era_ds.data_vars:\n", + " data_vars[varin] = ((\"time\", \"node\"), zero)\n", + " xr.Dataset(data_vars=data_vars, coords=coords).to_zarr(file_out, compute=False)\n", + "\n", + " # in the case of \"tstps\"\n", + " if ttype == \"step\":\n", + " t0 = pd.Timestamp(era_ds.time.values)\n", + " seconds = era_ds.step.values / 1e9\n", + " era_ds.time = pd.to_datetime(t0 + pd.Timedelta(seconds=seconds))\n", + "\n", + " # Iterate over nodes in chunks and write data to zarr file\n", + " for ins in range(0, nnodes, n_node_chunk):\n", + " end_node = min(ins + n_node_chunk, nnodes)\n", + " node_chunk = np.arange(ins, end_node)\n", + " mesh_chunk = mesh.isel(node=slice(ins, end_node))\n", + " era_chunk = subset_era_from_mesh(era_ds, mesh_chunk, input360=input360, gtype=gtype)\n", + "\n", + " # Get weights for interpolation\n", + " if gtype == \"grid\":\n", + " nx1d = len(era_chunk.longitude)\n", + " ny1d = len(era_chunk.latitude)\n", + " xx = np.tile(era_chunk.longitude, ny1d).reshape(ny1d, nx1d).T.ravel()\n", + " yy = np.tile(era_chunk.latitude, nx1d)\n", + " else: # for O1280 grids\n", + " xx = era_chunk.longitude\n", + " yy = era_chunk.latitude\n", + " era_chunk = era_chunk.drop_vars([\"number\", \"surface\"]) # useless for meteo exports\n", + "\n", + " in_xy = np.vstack((xx, yy)).T\n", + " if input360:\n", + " in_xy[:, 0][in_xy[:, 0] > 180] -= 360\n", + " out_xy = np.vstack((mesh_chunk.x, mesh_chunk.y)).T\n", + " vert, wgts, u_x, g_x = get_weights(in_xy, out_xy) # Assuming get_weights is defined elsewhere\n", + "\n", + " # Interpolate and write data for each variable and time chunk\n", + " for var_name in era_chunk.data_vars:\n", + " for it_chunk in range(0, ntimes, n_time_chunk):\n", + " t_end = min(it_chunk + n_time_chunk, ntimes)\n", + " time_chunk = era_chunk.time[it_chunk:t_end]\n", + " data_chunk = da.zeros((len(time_chunk), len(node_chunk)), chunks=(n_time_chunk, n_node_chunk))\n", + " for it, t_ in enumerate(time_chunk):\n", + " tmp = np.ravel(np.transpose(era_chunk.isel(time=it_chunk + it)[var_name].values))\n", + " data_chunk[it, :] = interp(tmp, vert, wgts, u_x, g_x) # Assuming interp is defined elsewhere\n", + " coords = {\"time\": time_chunk, \"node\": node_chunk}\n", + " ds = xr.Dataset({var_name: ((\"time\", \"node\"), data_chunk)}, coords=coords)\n", + " region = {\"time\": slice(it_chunk, t_end), \"node\": slice(ins, end_node)}\n", + " ds.to_zarr(file_out, mode=\"a-\", region=region)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the script is ready, let's do a sensitivity on the nodes & times " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "let's reduce first the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "era_test = era5.isel(time=slice(0,1000))" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "times_ = [50, 100, 200, 400, 1000]\n", + "nodes_ = [1000, 10000, len(renumbered_mesh.x)]\n", + "\n", + "time_perf = np.zeros((len(times_), len(nodes_)))\n", + "sizes = np.zeros((len(times_), len(nodes_)))\n", + "for i_t, ttime in enumerate(times_): \n", + " for i_n, nnode in enumerate(nodes_):\n", + " start = time.time()\n", + " write_meteo_on_mesh(era_test, renumbered_mesh, \"era5_2023_uvp.zarr\", ttime, nnode)\n", + " end = time.time()\n", + " time_perf[i_t,i_n] = end - start\n", + " # calculate size\n", + " bytes_per_element = zero.dtype.itemsize\n", + " sizes[i_t,i_n] = ttime * nnode * bytes_per_element / 1024 / 1000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "visualise the performances" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":Layout\n", + " .HeatMap.I :HeatMap [columns,index] (value)\n", + " .HeatMap.II :HeatMap [columns,index] (value)\n", + " .Curve.I :Curve [Size (MB)] (Time (sec))" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p2045" + } + }, + "output_type": "display_data" + } + ], + "source": [ + "# Convert data for pcolormesh plots into a format compatible with hvplot\n", + "time_perf_mesh_df = pd.DataFrame(time_perf, index=times_, columns=nodes_)\n", + "sizes_mesh_df = pd.DataFrame(sizes, index=times_, columns=nodes_)\n", + "\n", + "options = {\n", + " \"x\":'columns', \n", + " \"y\":'index', \n", + " \"C\":'value', \n", + " \"logx\" : True, \n", + " \"logy\" : True, \n", + " \"colorbar\" : True, \n", + " \"xlabel\" : \"chunk size for nodes\", \n", + " \"ylabel\" : \"chunk size for times\", \n", + " \"width\" : 500,\n", + "}\n", + "\n", + "# Convert data for line plot into a Pandas DataFrame\n", + "sizes_flat = sizes.ravel()\n", + "time_perf_flat = time_perf.ravel()\n", + "idx = np.argsort(sizes_flat)\n", + "line_df = pd.DataFrame({\n", + " 'Size (MB)': sizes_flat[idx],\n", + " 'Time (sec)': time_perf_flat[idx]\n", + "})\n", + "\n", + "# Create hvplot pcolormesh plots\n", + "time_perf_plot = time_perf_mesh_df.hvplot.heatmap(**options).opts(title = \"Time taken for exports (in seconds)\")\n", + "sizes_plot = sizes_mesh_df.hvplot.heatmap(**options).opts(title = \"Size of each chunk (in MB)\")\n", + "\n", + "# Create hvplot line plot\n", + "line_plot = line_df.hvplot.line(\n", + " x='Size (MB)', y='Time (sec)', \n", + " title='Time taken vs. Size of each chunk', width = 500)\n", + "\n", + "# Combine plots into a layout\n", + "layout = (time_perf_plot + sizes_plot + line_plot).cols(3)\n", + "\n", + "# Apply some options for better layout\n", + "layout.opts(\n", + " opts.HeatMap(colorbar=True),\n", + " opts.Layout(shared_axes=False, merge_tools=False),\n", + ")\n", + "# # Display the layout\n", + "hv.output(layout)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The best combination seems to be the **max number of nodes with the minimum number of times**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "check if it worked: " + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 2GB\n",
+       "Dimensions:        (node: 76064, time: 1000, triface: 147503, three: 3)\n",
+       "Coordinates:\n",
+       "    lat            (node) float32 304kB ...\n",
+       "    lon            (node) float32 304kB ...\n",
+       "  * node           (node) int64 609kB 0 1 2 3 4 ... 76060 76061 76062 76063\n",
+       "  * time           (time) datetime64[ns] 8kB 2023-01-01 ... 2023-02-11T15:00:00\n",
+       "Dimensions without coordinates: triface, three\n",
+       "Data variables:\n",
+       "    msl            (time, node) float64 609MB ...\n",
+       "    triface_nodes  (triface, three) int32 2MB ...\n",
+       "    u10            (time, node) float64 609MB ...\n",
+       "    v10            (time, node) float64 609MB ...
" + ], + "text/plain": [ + " Size: 2GB\n", + "Dimensions: (node: 76064, time: 1000, triface: 147503, three: 3)\n", + "Coordinates:\n", + " lat (node) float32 304kB ...\n", + " lon (node) float32 304kB ...\n", + " * node (node) int64 609kB 0 1 2 3 4 ... 76060 76061 76062 76063\n", + " * time (time) datetime64[ns] 8kB 2023-01-01 ... 2023-02-11T15:00:00\n", + "Dimensions without coordinates: triface, three\n", + "Data variables:\n", + " msl (time, node) float64 609MB ...\n", + " triface_nodes (triface, three) int32 2MB ...\n", + " u10 (time, node) float64 609MB ...\n", + " v10 (time, node) float64 609MB ..." + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = xr.open_dataset(\"era5_2023_uvp.zarr\", engine = \"zarr\")\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "27661f68e46a4d08b3e3405e4df744c1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "BokehModel(combine_events=True, render_bundle={'docs_json': {'7d813915-6c7a-4327-b1b5-a07c50422b67': {'version…" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import thalassa\n", + "import geoviews as gv\n", + "ds = thalassa.normalize(ds)\n", + "# Convert the node data in \"Tabular\" format: https://holoviews.org/user_guide/Tabular_Datasets.html\n", + "df = ds[[\"lon\", \"lat\", \"node\"]].to_dataframe().reset_index()\n", + "# Create a geoviews Points object\n", + "gv_points = gv.Points(df, kdims=[\"lon\", \"lat\"], vdims=[\"node\"]).opts(size = 1, color='k')\n", + "# Create a Dynamic map with the variable you want, e.g. \"depth\"\n", + "raster_dmap = thalassa.plot(ds.sel(time = \"2023-08-27\", method=\"nearest\"), \"u10\")\n", + "# now combine the raster with the points\n", + "combined_dmap = raster_dmap * gv_points\n", + "# plot\n", + "combined_dmap.opts(tools=[\"hover\"], width=1200, height=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### finally, convert to Selafin" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "def remove(path):\n", + " try:\n", + " if os.path.isfile(path):\n", + " os.remove(path) # Remove a file\n", + " elif os.path.isdir(path):\n", + " if not os.listdir(path): # Check if the directory is empty\n", + " os.rmdir(path)\n", + " else:\n", + " shutil.rmtree(path)\n", + " except OSError as e:\n", + " print(f\"Error: {e.strerror}\")\n", + " \n", + "\n", + "def write_meteo_selafin(outpath, input_atm_zarr):\n", + " xatm = xr.open_dataset(input_atm_zarr, engine=\"zarr\")\n", + " t0 = pd.Timestamp(xatm.time.values[0])\n", + " # Define a mapping from the original variable names to the new ones\n", + " var_map = {\n", + " \"u10\": (\"WINDX\", \"M/S\"),\n", + " \"v10\": (\"WINDY\", \"M/S\"),\n", + " \"msl\": (\"PATM\", \"PASCAL\"),\n", + " \"tmp\": (\"TAIR\", \"DEGREES C\"),\n", + " }\n", + " var_attrs = {}\n", + " for var in xatm.data_vars:\n", + " if var in var_map:\n", + " # Attributes for the variable\n", + " var_attrs[var] = (var_map[var][0], var_map[var][1])\n", + " # Add global attributes after concatenation\n", + " xatm.attrs[\"date_start\"] = [t0.year, t0.month, t0.day, t0.hour, t0.minute, t0.second]\n", + " xatm.attrs[\"ikle2\"] = xatm.triface_nodes.values + 1\n", + " xatm.attrs[\"variables\"] = {var: attrs for var, attrs in var_attrs.items()}\n", + " xatm = xatm.rename({\"lon\": \"x\", \"lat\": \"y\"})\n", + " xatm = xatm.drop_vars([\"triface_nodes\"])\n", + " xatm.selafin.write(outpath)\n", + " # remove(input_atm_zarr)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "write_meteo_selafin(\"era5_2023_uvp.slf\", \"era5_2023_uvp.zarr\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "test with O1280 grid: " + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 57GB\n",
+       "Dimensions:    (values: 6599680, time: 720)\n",
+       "Coordinates:\n",
+       "    latitude   (values) float64 53MB ...\n",
+       "    longitude  (values) float64 53MB ...\n",
+       "    number     int64 8B ...\n",
+       "    surface    float64 8B ...\n",
+       "  * time       (time) datetime64[ns] 6kB 2023-09-01 ... 2023-09-30T23:00:00\n",
+       "  * values     (values) int64 53MB 0 1 2 3 4 ... 6599676 6599677 6599678 6599679\n",
+       "Data variables:\n",
+       "    msl        (time, values) float32 19GB ...\n",
+       "    u10        (time, values) float32 19GB ...\n",
+       "    v10        (time, values) float32 19GB ...
" + ], + "text/plain": [ + " Size: 57GB\n", + "Dimensions: (values: 6599680, time: 720)\n", + "Coordinates:\n", + " latitude (values) float64 53MB ...\n", + " longitude (values) float64 53MB ...\n", + " number int64 8B ...\n", + " surface float64 8B ...\n", + " * time (time) datetime64[ns] 6kB 2023-09-01 ... 2023-09-30T23:00:00\n", + " * values (values) int64 53MB 0 1 2 3 4 ... 6599676 6599677 6599678 6599679\n", + "Data variables:\n", + " msl (time, values) float32 19GB ...\n", + " u10 (time, values) float32 19GB ...\n", + " v10 (time, values) float32 19GB ..." + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "O1280_test = xr.open_dataset(\"2023-09-01.uvp.O1280.zarr\", engine = \"zarr\")\n", + "O1280_test" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "nnode = len(renumbered_mesh.x)\n", + "write_meteo_on_mesh(O1280_test, renumbered_mesh, \"01280_v0.zarr\", 50, nnode, input360=True, gtype='tri')" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "write_meteo_selafin(\"01280_202309_uvp.slf\", \"01280_v0.zarr\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "it works !! (GIF you see at the beginning of the notebook)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/index.html b/index.html index 1e03316..d3f7c7d 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@