From 25d7103a3fbf06dbb2bb3ce3b379ca3062d9ca81 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Mon, 1 Feb 2021 17:57:37 -0700 Subject: [PATCH 001/169] Add separate script to extrapolate single variable Not dependent on exo to mpas conversion script --- landice/mesh_tools_li/extrapolate_variable.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 landice/mesh_tools_li/extrapolate_variable.py diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py new file mode 100644 index 000000000..e288340b1 --- /dev/null +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +""" +Script to extrapolate arbitrary variable + +Created on Mon Feb1 2021 + +@author: Matt Hoffman, Trevor Hillebrand +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +import numpy as np +from netCDF4 import Dataset +from optparse import OptionParser +import sys +from datetime import datetime + + +parser = OptionParser(description='Convert data from exodus file to MPAS mesh. WARNING: Change the SEACAS library dir to your own path! A simple usage example: conversion_exodus_init_to_mpasli_mesh.py -e antarctica.exo -o target.nc -a ./mpas_cellID.ascii -v beta') + +parser.add_option("-f", "--file", dest="nc_file", help="the mpas file to write to") +parser.add_option("-v", "--variable", dest="var_name", help="the mpas variable(s) you want to extrapolate") +parser.add_option("-m", "--method", dest="extrapolation", default='min', help="idw or min method of extrapolation") + +for option in parser.option_list: + if option.default != ("NO", "DEFAULT"): + option.help += (" " if option.help else "") + "[default: %default]" +options, args = parser.parse_args() + +dataset = Dataset(options.nc_file, 'r+') +var_name = options.var_name +varValue = dataset.variables[var_name][0, :] +extrapolation = options.extrapolation +# Extrapolation +nCells = len(dataset.dimensions['nCells']) +if 'thickness' in dataset.variables.keys(): + thickness = dataset.variables['thickness'][0,:] +cellsOnCell = dataset.variables['cellsOnCell'][:] +nEdgesOnCell = dataset.variables['nEdgesOnCell'][:] +xCell = dataset.variables["yCell"][:] +yCell = dataset.variables["xCell"][:] + + +keepCellMask = np.zeros((nCells,), dtype=np.int8) + +# Define region of good data to extrapolate from. Different methods for different variables +if var_name == "beta" or var_name == "muFriction": + keepCellMask[varValue > 0.0] = 1 + extrapolation == "min" +# find the mask for grounded ice region +else: + keepCellMask[thickness > 0.0] = 1 + +keepCellMaskNew = np.copy(keepCellMask) # make a copy to edit that will be used later +keepCellMaskOrig = np.copy(keepCellMask) # make a copy to edit that can be edited without changing the original + + +# recursive extrapolation steps: +# 1) find cell A with mask = 0 +# 2) find how many surrounding cells have nonzero mask, and their indices (this will give us the cells from upstream) +# 3) use the values for nonzero mask upstream cells to extrapolate the value for cell A +# 4) change the mask for A from 0 to 1 +# 5) Update mask +# 6) go to step 1) + +print("\nStart {} extrapolation using {} method".format(var_name, extrapolation)) +while np.count_nonzero(keepCellMask) != nCells: + + keepCellMask = np.copy(keepCellMaskNew) + searchCells = np.where(keepCellMask==0)[0] + varValueOld = np.copy(varValue) + + for iCell in searchCells: + neighborcellID = cellsOnCell[iCell,:nEdgesOnCell[iCell]]-1 + neighborcellID = neighborcellID[neighborcellID>=0] # Important: ignore the phantom "neighbors" that are off the edge of the mesh (0 values in cellsOnCell) + + mask_for_idx = keepCellMask[neighborcellID] # active cell mask + mask_nonzero_idx, = np.nonzero(mask_for_idx) + + nonzero_id = neighborcellID[mask_nonzero_idx] # id for nonzero beta cells + nonzero_num = np.count_nonzero(mask_for_idx) + + assert len(nonzero_id) == nonzero_num + + if nonzero_num > 0: + x_i = xCell[iCell] + y_i = yCell[iCell] + x_adj = xCell[nonzero_id] + y_adj = yCell[nonzero_id] + var_adj = varValueOld[nonzero_id] + if extrapolation == 'idw': + ds = np.sqrt((x_i-x_adj)**2+(y_i-y_adj)**2) + assert np.count_nonzero(ds)==len(ds) + var_interp = 1.0/sum(1.0/ds)*sum(1.0/ds*var_adj) + varValue[iCell] = var_interp + elif extrapolation == 'min': + varValue[iCell] = np.min(var_adj) + else: + sys.exit("ERROR: wrong extrapolation scheme! Set option x as idw or min!") + + keepCellMaskNew[iCell] = 1 + + print ("{0:8d} cells left for extrapolation in total {1:8d} cells".format(nCells-np.count_nonzero(keepCellMask), nCells)) +dataset.variables[var_name][0,:] = varValue # Put updated array back into file. +# === Clean-up ============= + +# Update history attribute of netCDF file +thiscommand = datetime.now().strftime("%a %b %d %H:%M:%S %Y") + ": " + " ".join(sys.argv[:]) +thiscommand = thiscommand+"; {} successfully extrapolated using the {} method".format(var_name, extrapolation) +if hasattr(dataset, 'history'): + newhist = '\n'.join([thiscommand, getattr(dataset, 'history')]) +else: + newhist = thiscommand +setattr(dataset, 'history', newhist) + + +dataset.close() From 743306d79ed5a6e80a76935247ea3d301ce87837 Mon Sep 17 00:00:00 2001 From: Trevor Ray Hillebrand Date: Wed, 20 Jul 2022 16:24:39 -0600 Subject: [PATCH 002/169] Use grounded and floating masks to define keepCellMask Depending on which field you want to extrapolate, use grounded or floating mask to define the values that are kept. --- landice/mesh_tools_li/extrapolate_variable.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py index e288340b1..539115367 100644 --- a/landice/mesh_tools_li/extrapolate_variable.py +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -35,26 +35,27 @@ nCells = len(dataset.dimensions['nCells']) if 'thickness' in dataset.variables.keys(): thickness = dataset.variables['thickness'][0,:] + bed = dataset.variables["bedTopography"][0,:] cellsOnCell = dataset.variables['cellsOnCell'][:] nEdgesOnCell = dataset.variables['nEdgesOnCell'][:] xCell = dataset.variables["yCell"][:] yCell = dataset.variables["xCell"][:] - -keepCellMask = np.zeros((nCells,), dtype=np.int8) - # Define region of good data to extrapolate from. Different methods for different variables -if var_name == "beta" or var_name == "muFriction": - keepCellMask[varValue > 0.0] = 1 +if var_name in ["effectivePressure", "beta", "muFriction"]: + groundedMask = (thickness > (-1028.0 / 910.0 * bed)) + keepCellMask = groundedMask * (varValue > 0.0) extrapolation == "min" -# find the mask for grounded ice region +elif var_name in ["floatingBasalMassBal"]: + floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) + keepCellMask = floatingMask * (varValue != 0.0) + extrapolation == "idw" else: - keepCellMask[thickness > 0.0] = 1 + keepCellMask = (thickness > 0.0) keepCellMaskNew = np.copy(keepCellMask) # make a copy to edit that will be used later keepCellMaskOrig = np.copy(keepCellMask) # make a copy to edit that can be edited without changing the original - # recursive extrapolation steps: # 1) find cell A with mask = 0 # 2) find how many surrounding cells have nonzero mask, and their indices (this will give us the cells from upstream) From 92793b13bc9df989ec9fe703a9d39a71ea3ea8ef Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Fri, 21 Oct 2022 13:44:51 -0700 Subject: [PATCH 003/169] Keep one cell beyond grounded mask before extrapolating Also do not allow any zero-valued cells in keepMask --- landice/mesh_tools_li/extrapolate_variable.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py index 539115367..71e146ec9 100644 --- a/landice/mesh_tools_li/extrapolate_variable.py +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -44,8 +44,16 @@ # Define region of good data to extrapolate from. Different methods for different variables if var_name in ["effectivePressure", "beta", "muFriction"]: groundedMask = (thickness > (-1028.0 / 910.0 * bed)) - keepCellMask = groundedMask * (varValue > 0.0) + keepCellMask = np.copy(groundedMask) extrapolation == "min" + + for iCell in range(nCells): + for n in range(nEdgesOnCell[iCell]): + jCell = cellsOnCell[iCell, n] - 1 + if (groundedMask[jCell] == 1): + keepCellMask[iCell] = 1 + continue + keepCellMask *= (varValue > 0) # ensure zero muFriction does not get extrapolated elif var_name in ["floatingBasalMassBal"]: floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) keepCellMask = floatingMask * (varValue != 0.0) From 345c7f6dab72c7bfccfc4a16567aa34e1833f897 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Fri, 16 Dec 2022 14:11:04 -0800 Subject: [PATCH 004/169] Add value option Add option to set all cells outside keepCellMask to a specific value --- landice/mesh_tools_li/extrapolate_variable.py | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py index 71e146ec9..d72f83fe2 100644 --- a/landice/mesh_tools_li/extrapolate_variable.py +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -20,7 +20,8 @@ parser.add_option("-f", "--file", dest="nc_file", help="the mpas file to write to") parser.add_option("-v", "--variable", dest="var_name", help="the mpas variable(s) you want to extrapolate") -parser.add_option("-m", "--method", dest="extrapolation", default='min', help="idw or min method of extrapolation") +parser.add_option("-m", "--method", dest="extrapolation", default='min', help="idw, min, or value method of extrapolation") +parser.add_option("-s", "--set_value", dest="set_value", default=None, help="value to set variable to outside keepCellMask when using -v value") for option in parser.option_list: if option.default != ("NO", "DEFAULT"): @@ -73,43 +74,46 @@ # 6) go to step 1) print("\nStart {} extrapolation using {} method".format(var_name, extrapolation)) -while np.count_nonzero(keepCellMask) != nCells: - - keepCellMask = np.copy(keepCellMaskNew) - searchCells = np.where(keepCellMask==0)[0] - varValueOld = np.copy(varValue) - - for iCell in searchCells: - neighborcellID = cellsOnCell[iCell,:nEdgesOnCell[iCell]]-1 - neighborcellID = neighborcellID[neighborcellID>=0] # Important: ignore the phantom "neighbors" that are off the edge of the mesh (0 values in cellsOnCell) - - mask_for_idx = keepCellMask[neighborcellID] # active cell mask - mask_nonzero_idx, = np.nonzero(mask_for_idx) - - nonzero_id = neighborcellID[mask_nonzero_idx] # id for nonzero beta cells - nonzero_num = np.count_nonzero(mask_for_idx) - - assert len(nonzero_id) == nonzero_num - - if nonzero_num > 0: - x_i = xCell[iCell] - y_i = yCell[iCell] - x_adj = xCell[nonzero_id] - y_adj = yCell[nonzero_id] - var_adj = varValueOld[nonzero_id] - if extrapolation == 'idw': - ds = np.sqrt((x_i-x_adj)**2+(y_i-y_adj)**2) - assert np.count_nonzero(ds)==len(ds) - var_interp = 1.0/sum(1.0/ds)*sum(1.0/ds*var_adj) - varValue[iCell] = var_interp - elif extrapolation == 'min': - varValue[iCell] = np.min(var_adj) - else: - sys.exit("ERROR: wrong extrapolation scheme! Set option x as idw or min!") - - keepCellMaskNew[iCell] = 1 - - print ("{0:8d} cells left for extrapolation in total {1:8d} cells".format(nCells-np.count_nonzero(keepCellMask), nCells)) +if extrapolation == 'value': + varValue[np.where(np.logical_not(keepCellMask))] = float(options.set_value) +else: + while np.count_nonzero(keepCellMask) != nCells: + keepCellMask = np.copy(keepCellMaskNew) + searchCells = np.where(keepCellMask==0)[0] + varValueOld = np.copy(varValue) + + for iCell in searchCells: + neighborcellID = cellsOnCell[iCell,:nEdgesOnCell[iCell]]-1 + neighborcellID = neighborcellID[neighborcellID>=0] # Important: ignore the phantom "neighbors" that are off the edge of the mesh (0 values in cellsOnCell) + + mask_for_idx = keepCellMask[neighborcellID] # active cell mask + mask_nonzero_idx, = np.nonzero(mask_for_idx) + + nonzero_id = neighborcellID[mask_nonzero_idx] # id for nonzero beta cells + nonzero_num = np.count_nonzero(mask_for_idx) + + assert len(nonzero_id) == nonzero_num + + if nonzero_num > 0: + x_i = xCell[iCell] + y_i = yCell[iCell] + x_adj = xCell[nonzero_id] + y_adj = yCell[nonzero_id] + var_adj = varValueOld[nonzero_id] + if extrapolation == 'idw': + ds = np.sqrt((x_i-x_adj)**2+(y_i-y_adj)**2) + assert np.count_nonzero(ds)==len(ds) + var_interp = 1.0/sum(1.0/ds)*sum(1.0/ds*var_adj) + varValue[iCell] = var_interp + elif extrapolation == 'min': + varValue[iCell] = np.min(var_adj) + else: + sys.exit("ERROR: wrong extrapolation scheme! Set option m as idw or min!") + + keepCellMaskNew[iCell] = 1 + + print ("{0:8d} cells left for extrapolation in total {1:8d} cells".format(nCells-np.count_nonzero(keepCellMask), nCells)) + dataset.variables[var_name][0,:] = varValue # Put updated array back into file. # === Clean-up ============= From aaf2982686981d913743ef57aa8c9a0bee72b7d3 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 28 Mar 2023 09:01:59 +0200 Subject: [PATCH 005/169] Break out Vector class in its own module --- conda_package/mpas_tools/vector.py | 234 +++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 conda_package/mpas_tools/vector.py diff --git a/conda_package/mpas_tools/vector.py b/conda_package/mpas_tools/vector.py new file mode 100644 index 000000000..c2914cfe4 --- /dev/null +++ b/conda_package/mpas_tools/vector.py @@ -0,0 +1,234 @@ +import numpy as np + + +class Vector: + """ + A class for representing Cartesian vectors with ``x``, ``y`` and ``z`` + components that are either ``float`` or ``numpy.array`` objects of + identical size. + + Attributes + ---------- + x : float or numpy.ndarray + The x component(s) + + y : float or numpy.ndarray + The y component(s) + + z : float or numpy.ndarray + The z component(s) + """ + def __init__(self, x, y, z): + """ + A class for representing Cartesian vectors with ``x``, ``y`` and ``z`` + components that are either ``float`` or ``numpy.array`` objects of + identical size. + + Parameters + ---------- + x : float or numpy.ndarray + The x component(s) + + y : float or numpy.ndarray + The y component(s) + + z : float or numpy.ndarray + The z component(s) + """ + self.x = x + self.y = y + self.z = z + + def angular_distance(self, other): + """ + Compute angular distance between points on the sphere, following: + https://en.wikipedia.org/wiki/Great-circle_distance + + Parameters + ---------- + other : mpas_tools.vector.Vector + The vector to compute the angular distance to + + Returns + ------- + angularDistance : numpy.ndarray + The angular distance (in radians) between segments of the transect. + """ + angular_distance = np.arctan2(self.cross(other).mag(), self.dot(other)) + return angular_distance + + @staticmethod + def intersects(a1, a2, b1, b2): + """ + Based on https://stackoverflow.com/a/26669130/7728169 + Determine if the great circle arc from ``a1`` to ``a2`` intersects that + from ``b1`` to ``b2``. + + Parameters + ---------- + a1 : mpas_tools.vector.Vector + Cartesian coordinates of the end point of a great circle arc. + The types of the attributes ``x``, ``y``, and ``z`` must either be + ``numpy.arrays`` of identical size for all 4 vectors (in which case + intersections are found element-wise), or scalars for + at least one of either ``a1`` and ``a2`` or ``b1`` and ``b2``. + + a2 : mpas_tools.vector.Vector + Cartesian coordinates of the other end point of a great circle arc. + + b1 : mpas_tools.vector.Vector + Cartesian coordinates of an end point of a second great circle arc. + + b2 : mpas_tools.vector.Vector + Cartesian coordinates of the other end point of the second great + circle arc. + + Returns + ------- + intersect : numpy.ndarray + A boolean array of the same size as ``a1`` and ``a2`` or ``b1`` and + ``b2``, whichever is greater, indicating if the particular pair of + arcs intersects + """ + return np.logical_and(Vector.straddles(a1, a2, b1, b2), + Vector.straddles(b1, b2, a1, a2)) + + @staticmethod + def intersection(a1, a2, b1, b2): + """ + Based on https://stackoverflow.com/a/26669130/7728169 + Find the intersection point as a unit vector between great circle arc + from ``a1`` to ``a2`` and from ``b1`` to ``b2``. The arcs should have + already have been found to intersect by calling ``intersects()`` + + Parameters + ---------- + a1 : mpas_tools.vector.Vector + Cartesian coordinates of the end point of a great circle arc. + The types of the attributes ``x``, ``y``, and ``z`` must either be + ``numpy.arrays`` of identical size for all 4 vectors (in which case + intersections are found element-wise), or scalars for + at least one of either ``a1`` and ``a2`` or ``b1`` and ``b2``. + + a2 : mpas_tools.vector.Vector + Cartesian coordinates of the other end point of a great circle arc. + + b1 : mpas_tools.vector.Vector + Cartesian coordinates of an end point of a second great circle arc. + + b2 : mpas_tools.vector.Vector + Cartesian coordinates of the other end point of the second great + circle arc. + + Returns + ------- + points : mpas_tools.vector.Vector + An array of Cartesian points *on the unit sphere* indicating where + the arcs intersect + """ + points = (a1.cross(a2)).cross(b1.cross(b2)) + s = np.sign(Vector.det(a1, b1, b2))/points.mag() + points = Vector(s*points.x, s*points.y, s*points.z) + return points + + @staticmethod + def straddles(a1, a2, b1, b2): + """ + Based on https://stackoverflow.com/a/26669130/7728169 + Determines if the great circle segment determined by (a1, a2) + straddles the great circle determined by (b1, b2) + + Parameters + ---------- + a1: mpas_tools.vector.Vector + Cartesian coordinates of first end point of first great circle arc. + The types of the attributes ``x``, ``y``, and ``z`` must either be + ``numpy.arrays`` of identical size for all 4 vectors (in which case + intersections are found element-wise), or scalars for + at least one of either the ``a``s or the ``b``s. + + a2 : mpas_tools.vector.Vector + Second end point of first great circle arc. + + b1 : mpas_tools.vector.Vector + First end point of second great circle arc. + + b2 : mpas_tools.vector.Vector + Second end point of second great circle arc. + + Returns + ------- + straddle : numpy.ndarray + A boolean array of the same size as the ``a``s or the ``b``s, whichever + is greater, indicating if the great circle segment determined by + (a1, a2) straddles the great circle determined by (b1, b2) + """ + return Vector.det(a1, b1, b2) * Vector.det(a2, b1, b2) < 0 + + def dot(self, other): + """ + Compute the dot product between this vector and ``other``. + + Parameters + ---------- + other : mpas_tools.vector.Vector + The other vector + + Returns + ------- + dot_product : numpy.ndarray + The dot product + """ + return self.x * other.x + self.y * other.y + self.z * other.z + + def cross(self, other): + """ + Compute the dot product between this vector and ``other``. + + Parameters + ---------- + other : mpas_tools.vector.Vector + The other vector + + Returns + ------- + cross_product : mpas_tools.vector.Vector + The cross product + """ + return Vector(self.y * other.z - self.z * other.y, + self.z * other.x - self.x * other.z, + self.x * other.y - self.y * other.x) + + @staticmethod + def det(v1, v2, v3): + """ + The determinant of the matrix defined by the three ``Vector`` objects + + Parameters + ---------- + v1 : mpas_tools.vector.Vector + First row of the matrix + + v2 : mpas_tools.vector.Vector + Second row + + v3 : mpas_tools.vector.Vector + Third row + + Returns + ------- + determinant : numpy.ndarray + The determinant of the matrix + """ + return v1.dot(v2.cross(v3)) + + def mag(self): + """ + The magnitude of the vector + + Returns + ------- + magnitude : numpy.ndarray + The magnitude of the vector + """ + return np.sqrt(self.dot(self)) From 14cf5a2ee0378c4459a8006974f08074ea039dbf Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 28 Mar 2023 09:18:14 +0200 Subject: [PATCH 006/169] Switch to separate vector class --- .../mpas_tools/tests/test_transects.py | 21 ++- conda_package/mpas_tools/transects.py | 168 ++---------------- conda_package/mpas_tools/viz/transects.py | 27 +-- 3 files changed, 41 insertions(+), 175 deletions(-) diff --git a/conda_package/mpas_tools/tests/test_transects.py b/conda_package/mpas_tools/tests/test_transects.py index e3e417647..1ac57ce59 100755 --- a/conda_package/mpas_tools/tests/test_transects.py +++ b/conda_package/mpas_tools/tests/test_transects.py @@ -3,9 +3,14 @@ import numpy as np from mpas_tools.cime.constants import constants -from mpas_tools.transects import subdivide_great_circle, \ - cartesian_to_great_circle_distance, subdivide_planar, \ - lon_lat_to_cartesian, angular_distance, intersects, intersection, Vector +from mpas_tools.transects import ( + angular_distance, + cartesian_to_great_circle_distance, + lon_lat_to_cartesian, + subdivide_great_circle, + subdivide_planar +) +from mpas_tools.vector import Vector def test_subdivide_great_circle(): @@ -41,7 +46,7 @@ def test_angular_distance_first_second(): ang_dist1 = angular_distance(x=x, y=y, z=z) first = Vector(x[0:-1], y[0:-1], z[0:-1]) second = Vector(x[1:], y[1:], z[1:]) - ang_dist2 = angular_distance(first=first, second=second) + ang_dist2 = first.angular_distance(second) assert np.all(ang_dist1 == ang_dist2) @@ -50,7 +55,7 @@ def test_intersects_scalar(): a2 = _lon_lat_to_vector(10., 10.) b1 = _lon_lat_to_vector(-10., 10.) b2 = _lon_lat_to_vector(10., -10.) - assert intersects(a1, a2, b1, b2) + assert Vector.intersects(a1, a2, b1, b2) def test_intersects_array(): @@ -58,7 +63,7 @@ def test_intersects_array(): a2 = _lon_lat_to_vector(np.array([-5., 10.]), np.array([-5., 10.])) b1 = _lon_lat_to_vector(-10., 10.) b2 = _lon_lat_to_vector(10., -10.) - result = intersects(a1, a2, b1, b2) + result = Vector.intersects(a1, a2, b1, b2) assert np.all(result == np.array([False, True])) @@ -69,7 +74,7 @@ def test_intersection_scalar(): b2 = _lon_lat_to_vector(10., -10.) earth_radius = constants['SHR_CONST_REARTH'] cross = _lon_lat_to_vector(0., 0.) - point = intersection(a1, a2, b1, b2) + point = Vector.intersection(a1, a2, b1, b2) assert np.all(np.isclose( [earth_radius*point.x, earth_radius*point.y, earth_radius*point.z], [cross.x, cross.y, cross.z])) @@ -82,7 +87,7 @@ def test_intersection_array(): b2 = _lon_lat_to_vector(10., -10.) earth_radius = constants['SHR_CONST_REARTH'] cross = _lon_lat_to_vector(0., 0.) - point = intersection(a1, a2, b1, b2) + point = Vector.intersection(a1, a2, b1, b2) assert np.all(np.isclose( [earth_radius*point.x[0], earth_radius*point.y[0], earth_radius*point.z[0]], diff --git a/conda_package/mpas_tools/transects.py b/conda_package/mpas_tools/transects.py index 60276ccd0..67a186262 100644 --- a/conda_package/mpas_tools/transects.py +++ b/conda_package/mpas_tools/transects.py @@ -1,6 +1,8 @@ import numpy from shapely.geometry import LineString, Point +from mpas_tools.vector import Vector + def subdivide_great_circle(x, y, z, maxRes, earthRadius): """ @@ -116,7 +118,7 @@ def cartesian_to_great_circle_distance(x, y, z, earth_radius): transectv1 = Vector(x[segIndex+1], y[segIndex+1], z[segIndex+1]) distance[segIndex+1] = distance[segIndex] + \ - earth_radius*angular_distance(first=transectv0, second=transectv1) + earth_radius*transectv0.angular_distance(transectv1) return distance @@ -213,174 +215,30 @@ def cartesian_to_lon_lat(x, y, z, earth_radius, degrees): return lon, lat -def angular_distance(x=None, y=None, z=None, first=None, second=None): +def angular_distance(x, y, z): """ Compute angular distance between points on the sphere, following: https://en.wikipedia.org/wiki/Great-circle_distance Parameters ---------- - x : numpy.ndarray, optional + x : numpy.ndarray The Cartesian x coordinate of a transect, where the number of segments - is ``len(x) - 1``. ``x``, ``y`` and ``z`` are of the same length and - all must be present if ``first`` and ``second`` are not provided. + is ``len(x) - 1``. ``x``, ``y`` and ``z`` are of the same lengt. - y : numpy.ndarray, optional + y : numpy.ndarray The Cartesian y coordinate of the transect - z : numpy.ndarray, optional + z : numpy.ndarray The Cartesian z coordinate of the transect - first : mpas_tools.transect.Vector, optional - The start points of each segment of the transect, where the - ``x``, ``y``, and ``z`` attributes of each vector are numpy.ndarray - objects. - - second : mpas_tools.transect.Vector, optional - The end points of each segment of the transect - Returns ------- - angularDistance : numpy.ndarray + distance : numpy.ndarray The angular distance (in radians) between segments of the transect. """ - if first is None or second is None: - first = Vector(x[0:-1], y[0:-1], z[0:-1]) - second = Vector(x[1:], y[1:], z[1:]) + first = Vector(x[0:-1], y[0:-1], z[0:-1]) + second = Vector(x[1:], y[1:], z[1:]) - angularDistance = numpy.arctan2(_mag(_cross(first, second)), - _dot(first, second)) - - return angularDistance - - -def intersects(a1, a2, b1, b2): - """ - Based on https://stackoverflow.com/a/26669130/7728169 - Determine if the great circle arc from ``a1`` to ``a2`` intersects that - from ``b1`` to ``b2``. - - Parameters - ---------- - a1 : mpas_tools.transects.Vector - Cartesian coordinates of the end point of a great circle arc. - The types of the attributes ``x``, ``y``, and ``z`` must either be - ``numpy.arrays`` of identical size for all 4 vectors (in which case - intersections are found element-wise), or scalars for - at least one of either ``a1`` and ``a2`` or ``b1`` and ``b2``. - - a2 : mpas_tools.transects.Vector - Cartesian coordinates of the other end point of a great circle arc. - - b1 : mpas_tools.transects.Vector - Cartesian coordinates of an end point of a second great circle arc. - - b2 : mpas_tools.transects.Vector - Cartesian coordinates of the other end point of the second great circle - arc. - - Returns - ------- - intersect : numpy.ndarray - A boolean array of the same size as ``a1`` and ``a2`` or ``b1`` and - ``b2``, whichever is greater, indicating if the particular pair of arcs - intersects - """ - return numpy.logical_and(_straddles(a1, a2, b1, b2), - _straddles(b1, b2, a1, a2)) - - -def intersection(a1, a2, b1, b2): - """ - Based on https://stackoverflow.com/a/26669130/7728169 - Find the intersection point as a unit vector between great circle arc from - ``a1`` to ``a2`` and from ``b1`` to ``b2``. The arcs should have already - have been found to intersect by calling ``intersects()`` - - Parameters - ---------- - a1 : mpas_tools.transects.Vector - Cartesian coordinates of the end point of a great circle arc. - The types of the attributes ``x``, ``y``, and ``z`` must either be - ``numpy.arrays`` of identical size for all 4 vectors (in which case - intersections are found element-wise), or scalars for - at least one of either ``a1`` and ``a2`` or ``b1`` and ``b2``. - - a2 : mpas_tools.transects.Vector - Cartesian coordinates of the other end point of a great circle arc. - - b1 : mpas_tools.transects.Vector - Cartesian coordinates of an end point of a second great circle arc. - - b2 : mpas_tools.transects.Vector - Cartesian coordinates of the other end point of the second great circle - arc. - Returns - ------- - points : mpas_tools.transects.Vector - An array of Cartesian points *on the unit sphere* indicating where the - arcs intersect - """ - points = _cross(_cross(a1, a2), _cross(b1, b2)) - s = numpy.sign(_det(a1, b1, b2))/_mag(points) - points = Vector(s*points.x, s*points.y, s*points.z) - return points - - -class Vector: - """ - A class for representing Cartesian vectors with ``x``, ``y`` and ``z`` - components that are either ``float`` or ``numpy.array`` objects of identical - size. - """ - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - - -def _straddles(a1, a2, b1, b2): - """ - Based on https://stackoverflow.com/a/26669130/7728169 - Determines if the great circle segment determined by (a1, a2) - straddles the great circle determined by (b1, b2) - - Parameters - ---------- - a1, a2, b1, b2 : Vector - Cartesian coordinates of the end points of two great circle arcs. - The types of the attributes ``x``, ``y``, and ``z`` must either be - ``numpy.arrays`` of identical size for all 4 vectors (in which case - intersections are found element-wise), or scalars for - at least one of either the ``a``s or the ``b``s. - - Returns - ------- - straddle : numpy.ndarray - A boolean array of the same size as the ``a``s or the ``b``s, whichever - is greater, indicating if the great circle segment determined by - (a1, a2) straddles the great circle determined by (b1, b2) - """ - return _det(a1, b1, b2) * _det(a2, b1, b2) < 0 - - -def _dot(v1, v2): - """The dot product between two ``Vector`` objects ``v1`` and ``v2``""" - return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z - - -def _cross(v1, v2): - """The cross product between two ``Vector`` objects ``v1`` and ``v2``""" - return Vector(v1.y * v2.z - v1.z * v2.y, - v1.z * v2.x - v1.x * v2.z, - v1.x * v2.y - v1.y * v2.x) - - -def _det(v1, v2, v3): - """The determinant of the matrix defined by the three ``Vector`` objects""" - return _dot(v1, _cross(v2, v3)) - - -def _mag(v): - """The magnitude of the ``Vector`` object ``v``""" - return numpy.sqrt(_dot(v, v)) + distance = first.angular_distance(second) + return distance diff --git a/conda_package/mpas_tools/viz/transects.py b/conda_package/mpas_tools/viz/transects.py index d0b7df1aa..2a3aff0d0 100644 --- a/conda_package/mpas_tools/viz/transects.py +++ b/conda_package/mpas_tools/viz/transects.py @@ -4,9 +4,13 @@ from scipy.spatial import cKDTree from shapely.geometry import LineString, Point -from mpas_tools.transects import Vector, lon_lat_to_cartesian, \ - cartesian_to_lon_lat, intersects, intersection, angular_distance, \ - subdivide_great_circle, subdivide_planar +from mpas_tools.transects import ( + cartesian_to_lon_lat, + lon_lat_to_cartesian, + subdivide_great_circle, + subdivide_planar +) +from mpas_tools.vector import Vector def make_triangle_tree(dsTris): @@ -194,8 +198,8 @@ def find_transect_cells_and_weights(lonTransect, latTransect, dsTris, dsMesh, yNode[n1IndicesCand], zNode[n1IndicesCand]) - intersect = intersects(n0Cand, n1Cand, transectv0, - transectv1) + intersect = Vector.intersects(n0Cand, n1Cand, transectv0, + transectv1) n0Inter = Vector(n0Cand.x[intersect], n0Cand.y[intersect], @@ -208,24 +212,23 @@ def find_transect_cells_and_weights(lonTransect, latTransect, dsTris, dsMesh, n0IndicesInter = n0IndicesCand[intersect] n1IndicesInter = n1IndicesCand[intersect] - intersections = intersection(n0Inter, n1Inter, transectv0, transectv1) + intersections = Vector.intersection(n0Inter, n1Inter, transectv0, + transectv1) intersections = Vector(earth_radius*intersections.x, earth_radius*intersections.y, earth_radius*intersections.z) - angularDistance = angular_distance(first=transectv0, - second=intersections) + angularDistance = transectv0.angular_distance(intersections) dNodeLocal = dStart + earth_radius * angularDistance - dStart += earth_radius*angular_distance(first=transectv0, - second=transectv1) + dStart += earth_radius*transectv0.angular_distance(transectv1) node0Inter = numpy.mod(n0IndicesInter, nNodes) node1Inter = numpy.mod(n1IndicesInter, nNodes) - nodeWeights = (angular_distance(first=intersections, second=n1Inter) / - angular_distance(first=n0Inter, second=n1Inter)) + nodeWeights = (intersections.angular_distance(n1Inter) / + n0Inter.angular_distance(n1Inter)) weights = numpy.zeros((len(trisInter), nHorizWeights)) cellIndices = numpy.zeros((len(trisInter), nHorizWeights), int) From 0a5e1a69216f930abea389a2b4c28ab5d5ce86a2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 18 May 2023 17:52:58 -0600 Subject: [PATCH 007/169] Fix culling in VTK extractor with multiple times If here are multiple time slices per file and you try to cull the files to a region, we want to only cull the unique files. Previously, the tool was culling the same file repeatedly, once for each time index. --- conda_package/mpas_tools/viz/paraview_extractor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/conda_package/mpas_tools/viz/paraview_extractor.py b/conda_package/mpas_tools/viz/paraview_extractor.py index 629608f50..6fa33a2ae 100644 --- a/conda_package/mpas_tools/viz/paraview_extractor.py +++ b/conda_package/mpas_tools/viz/paraview_extractor.py @@ -2173,9 +2173,13 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names, bar = None out_time_file_names = [] + already_culled = [] for index, filename in enumerate(time_file_names): - out_filename = '{}/time_series{:04d}.nc'.format(temp_dir, index) + file_index = len(already_culled) + out_filename = f'{temp_dir}/time_series{file_index:04d}.nc' out_time_file_names.append(out_filename) + if filename in already_culled: + continue ds_in = xarray.open_dataset(filename) ds_out = xarray.Dataset() if xtime is None: @@ -2188,6 +2192,7 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names, if dim in ds_in[var].dims: ds_out[var] = ds_in[var].where(region_masks[dim], drop=True) write_netcdf(ds_out, out_filename) + already_culled.append(filename) if use_progress_bar: bar.update(index+1) bar.finish() From 4185a9b71d035f4c1990132d99d01074dbb4845e Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 11 Jun 2023 13:39:09 -0600 Subject: [PATCH 008/169] Fix `SHR_CONST_OMEGA` in `cime.constants` Update testing to do a better job at skipping expressions when verifying constants. --- conda_package/mpas_tools/cime/constants.py | 10 ++++-- .../mpas_tools/tests/test_cime_constants.py | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/conda_package/mpas_tools/cime/constants.py b/conda_package/mpas_tools/cime/constants.py index b8fc3fd45..d550c362b 100644 --- a/conda_package/mpas_tools/cime/constants.py +++ b/conda_package/mpas_tools/cime/constants.py @@ -1,8 +1,10 @@ +import numpy as np + + constants = \ {'SHR_CONST_PI': 3.14159265358979323846, 'SHR_CONST_CDAY': 86400.0, 'SHR_CONST_SDAY': 86164.0, - 'SHR_CONST_OMEGA': 2.0, 'SHR_CONST_REARTH': 6.37122e6, 'SHR_CONST_G': 9.80616, 'SHR_CONST_STEBOL': 5.67e-8, @@ -34,8 +36,8 @@ 'SHR_CONST_OCN_REF_SAL': 34.7, 'SHR_CONST_ICE_REF_SAL': 4.0, 'SHR_CONST_SPVAL': 1.0e30, - 'SHR_CONST_SPVAL_TOLMIN': 0.99, - 'SHR_CONST_SPVAL_TOLMAX': 1.01, + 'SHR_CONST_SPVAL_TOLMIN': 0.99 * 1.0e30, + 'SHR_CONST_SPVAL_TOLMAX': 1.01 * 1.0e30, 'SHR_CONST_SPVAL_AERODEP': 1.e29, 'SHR_CONST_VSMOW_18O': 2005.2e-6, 'SHR_CONST_VSMOW_17O': 379.e-6, @@ -44,3 +46,5 @@ 'SHR_CONST_VSMOW_T': 1.85e-6, 'SHR_CONST_VSMOW_H': 0.99984426, 'SHR_CONST_RSTD_H2ODEV': 1.0} + +constants['SHR_CONST_OMEGA'] = 2.0 * np.pi / constants['SHR_CONST_SDAY'] diff --git a/conda_package/mpas_tools/tests/test_cime_constants.py b/conda_package/mpas_tools/tests/test_cime_constants.py index 9e26b53fa..25f0e140d 100644 --- a/conda_package/mpas_tools/tests/test_cime_constants.py +++ b/conda_package/mpas_tools/tests/test_cime_constants.py @@ -13,8 +13,8 @@ def test_cime_constants(e3sm_tag='master'): """ resp = requests.get( - 'https://raw.githubusercontent.com/E3SM-Project/E3SM/{}/share/util/' - 'shr_const_mod.F90'.format(e3sm_tag)) + f'https://raw.githubusercontent.com/E3SM-Project/E3SM/{e3sm_tag}/' + f'share/util/shr_const_mod.F90') text = resp.text @@ -28,20 +28,28 @@ def test_cime_constants(e3sm_tag='master'): constant, value = _parse_value(line) if constant is None: continue - print(line) - print('parsed: {} = {}'.format(constant, value)) + print(f'line: {line}') + print(f'parsed: {constant} = {value}') if constant in constants: - print('verifying {}'.format(constant)) - assert value == constants[constant] + if isinstance(value, float): + print('verifying {}'.format(constant)) + assert value == constants[constant] + else: + print('skipping verification for {}'.format(constant)) + found[constant] = True + else: + print('not in constants') + + print('') - allFound = True + all_found = True for constant in found: if not found[constant]: print('{} was not found!'.format(constant)) - allFound = False + all_found = False - assert allFound + assert all_found def _parse_value(line): @@ -57,11 +65,7 @@ def _parse_value(line): if '!' in line: line, _ = line.split('!', 1) - if '_R8' in line: - line, _ = line.split('_R8') - - if '_r8' in line: - line, _ = line.split('_r8') + line = line.replace('_R8', '').replace('_r8', '') try: value = float(line) From b563e8d121b9bb954e5cd46dd2fd87b900ce4a9a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 16 Jun 2023 13:33:08 +0200 Subject: [PATCH 009/169] Update to v0.21.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index ebf1a5397..975203982 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -33,6 +33,7 @@ Documentation On GitHub `v0.18.0`_ `0.18.0`_ `v0.19.0`_ `0.19.0`_ `v0.20.0`_ `0.20.0`_ +`v0.21.0`_ `0.21.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -89,3 +90,5 @@ Documentation On GitHub .. _`0.19.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.19.0 .. _`v0.20.0`: ../0.20.0/index.html .. _`0.20.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.20.0 +.. _`v0.21.0`: ../0.21.0/index.html +.. _`0.21.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.21.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 8a79de981..9f074532b 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 20, 0) +__version_info__ = (0, 21, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 3ba878a3a..ab3bb74c5 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.20.0" %} +{% set version = "0.21.0" %} package: name: {{ name|lower }} From 7456638a5dd4feee794e37b63c1065ae2a592c24 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 18 May 2023 15:58:27 -0600 Subject: [PATCH 010/169] Add mesh conversion tools with NetCDF-C These work well even for very large meshes. The mask creator has not yet been ported and is not included. --- .../CMakeLists.txt | 15 + .../mesh_conversion_tools_netcdf_c/README | 132 + .../mesh_conversion_tools_netcdf_c/edge.h | 32 + .../json/json-forwards.h | 284 + .../json/json.h | 2075 +++++++ .../jsoncpp.cpp | 5192 +++++++++++++++++ .../mpas_cell_culler.cpp | 1483 +++++ .../mpas_mesh_converter.cpp | 3231 ++++++++++ .../netcdf_utils.h | 494 ++ .../mesh_conversion_tools_netcdf_c/pnt.h | 509 ++ .../string_utils.h | 104 + 11 files changed, 13551 insertions(+) create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/CMakeLists.txt create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/README create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/edge.h create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h create mode 100755 mesh_tools/mesh_conversion_tools_netcdf_c/string_utils.h diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/CMakeLists.txt b/mesh_tools/mesh_conversion_tools_netcdf_c/CMakeLists.txt new file mode 100755 index 000000000..c1473502c --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required (VERSION 3.0.2) +project (mesh_conversion_tools) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + +add_executable (MpasMeshConverter.x mpas_mesh_converter.cpp) +target_link_libraries (MpasMeshConverter.x netcdf) + +add_executable (MpasCellCuller.x mpas_cell_culler.cpp) +target_link_libraries (MpasCellCuller.x netcdf) + +#add_executable (MpasMaskCreator.x mpas_mask_creator.cpp jsoncpp.cpp) +#target_link_libraries (MpasMaskCreator.x netcdf) + +install (TARGETS MpasMeshConverter.x MpasCellCuller.x DESTINATION bin) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/README b/mesh_tools/mesh_conversion_tools_netcdf_c/README new file mode 100755 index 000000000..5bfb93cd6 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/README @@ -0,0 +1,132 @@ +Readme for mpas_mesh_converter.cpp and mpas_cell_culler.cpp + +Author: Doug Jacobsen + +Purpose: + mpas_mesh_converter.cpp is a piece of software designed create an MPAS mesh. + As input, this software takes the locations of MPAS cell centers, and cell + vertices, along with the connectivity array cellsOnVertex. If provided, it + will also migrate data from the meshDensity field, if it's not present it + will write 1.0 for every cell. + + mpas_cell_culler.cpp is a piece of software designed remove + cells/edge/vertices from an MPAS mesh. As input, this software takes a + valid MPAS mesh with one additional field "cullCell". This new field should + be nCells integers. A 1 means the cell should be kept, and a 0 means the + cell should be removed. + + mpas_mask_creator.cpp is a piece of software designed to create cell masks + from region definitions. Region definitions are defined in geojson files, + and can be created using the tools contained within the repository located + at: + https://github.com/MPAS-Dev/geometric_features + Masks have a value of 0 or 1, and are integers. + +Requirements: + mpas_mesh_converter.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It also requires the tr1 headers for c++, specifically unordered_set. + It has been tested using g++ version 4.8.1 + + mpas_cell_culler.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It has been tested using g++ version 4.8.1 + + mpas_mask_creator.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It has been tested using g++ version 4.8.1 + +Usage of mpas_mesh_converter.cpp: + ./MpasMeshConverter.x [input_name] [output_name] + + input_name: + The input_name should be the name of a NetCDF file containing the following information. + Dimensions: + nCells - The number of cells described in the file. + nVertices - The number of vertices described in the file. + vertexDegree - The maximum number of cells that surround each vertex. + Variables: + xCell -- The x location of every cell center. + yCell -- The y location of every cell center. + zCell -- The z location of every cell center. + xVertex -- The x location of every vertex (cell corner). + yVertex -- The y location of every vertex (cell corner). + zVertex -- The z location of every vertex (cell corner). + cellsOnVertex -- A list of cell indices that border each vertex (Dimensioned vertexDegree * nVertices). + This list can contain -1 values if the dual cell is not complete (e.g. a line rather than a triangle). + meshDensity -- (Optional) The value of the generating density function at each cell center. + If this variable is ommitted, it is filled with 1.0 on output. + Global Attributes: + on_a_sphere -- Should be "YES" if the cells/vertices are defined on a sphere and "NO" if they are defined in a plane. + history -- (Optional) Should be defined to a string describing how the input file was generated. + mesh_id -- (Optional) Should be a 40 character string (Alpha-Numeric) that can be used to identify the mesh. + If ommitted, a random string will be placed in the output file. + output_name: + The output_name should be the name of the NetCDF file that will be generated. It will be a valid MPAS mesh. + No initial conditions are placed in this file. If input_name is specified, output_name defaults to mesh.nc. + + +Usage of mpas_cell_culler.cpp: + ./MpasCellCuller.x [input_name] [output_name] [[-m/-i] mask_file] [-c] + + input_name: + The input name should be the name of a NetCDF file that is a fully valid MPAS file. + It is required to have all of the fields an output file from MpasMeshConverter.x contains. + There is one additional field: + cullCell -- (Optional) Should be dimensioned nCells. Should contain a 1 for all cells that should be removed + and a 0 for all cells that should be kept. + + output_name: + The output_name should be the name of the NetCDF file that will be generated. It will be the same as the input file, + with cells, edges, and vertices removed where appropriate. Also, the cullCell field will be removed. + If input_name is specified, outputname defaults to culled_mesh.nc. + + -m/-i: + These flags allow the addition of a masks file to be used to define + cells that should be culled. + If the -m flag is used, the masks file is used to cull the mesh. + Meaning, where any mask in the masks file is 1, the mesh will be + culled. + If the -i flag is used, the inverse of the masks file is used to cull + the mesh. Meaning where all masks in the masks file are 0, the mesh + will be culled. + + mask_file: + The mask_file argument should be the name of a NetCDF file containing + the regionCellMasks field, which defines masks for culling cells. + + -c: + Output the mapping from old to new mesh (cellMap) in cellMapForward.txt, + and output the reverse mapping from new to old mesh in cellMapBackward.txt. + +Usage of mpas_mask_creator.cpp: + ./MpasMaskCreator.x [input_mesh] [output_masks] [feature_group1] [feature_group2] ... [feature_groupN] + + input_name: + The input name should be the path to a NetCDF file that is a fully valid MPAS file. + + output_masks: + The output masks should be the path to a NetCDF file that will be + generated. It will contain all of the mask information after applying + the region specifications to the input name mesh file. + + feature_groupN: + After the output masks argument, the mask creator can take an arbitrary + number of feature files. The mask creator assumes each file is a group + of features (regions, transects, point data, etc.). Each file must be + formatted as geojson, and the name of each group should be contained + within the file. + + The mask creator assumes the latitude and longitude contained within + each geojson file are given in degrees, and that the latitude ranges + from -90 to 90, while the longitude ranges from -180 to 180. + +Notes for mpas_mesh_converter.cpp: + - The output mesh should have an attribute "mesh_spec" which defined which + version of the MPAS Mesh Specification this mesh conforms to. + - Cells that are not complete (e.g. are not surrounded by vertices) will be + given a negative area. + - Negative area cells can be removed at a later state to remove non-complete + cells. + + +Notes for mpas_cell_culler.cpp: + - Cells that contain negative area will be removed in addition to all cells that have + a cullCell value of 1. diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h b/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h new file mode 100755 index 000000000..dbf2d8789 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h @@ -0,0 +1,32 @@ + +class edge {/*{{{*/ + public: + int cell1, cell2; + int vertex1, vertex2; + int idx; + + edge() + : cell1(-1), cell2(-1), vertex1(-1), vertex2(-1), idx(-1) { } + + struct edge_hasher {/*{{{*/ + size_t operator()(const edge &edge_) const { + uint32_t hash; + size_t i, key[2] = { (size_t)edge_.vertex1, (size_t)edge_.vertex2 }; + for(hash = i = 0; i < sizeof(key); ++i) { + hash += ((uint8_t *)key)[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + };/*}}}*/ + + bool operator==(const edge &e) const {/*{{{*/ + return (vertex1 == e.vertex1) && (vertex2 == e.vertex2); + }/*}}}*/ +};/*}}}*/ + + diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h new file mode 100755 index 000000000..910c7de97 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h @@ -0,0 +1,284 @@ +/// Json-cpp amalgated forward header (http://jsoncpp.sourceforge.net/). +/// It is intended to be used with #include "json/json-forwards.h" +/// This header provides forward declaration for all JsonCpp types. + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + +/* +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... + +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. + +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is +released under the terms of the MIT License (see below). + +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. + +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: + + http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +======================================================================== +Copyright (c) 2007-2010 Baptiste Lepilleur + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +======================================================================== +(END LICENSE TEXT) + +The MIT license is compatible with both the GPL and commercial +software, affording one all of the rights of Public Domain with the +minor nuisance of being required to keep the above copyright notice +and license text in the source code. Note also that by accepting the +Public Domain "license" you can re-license your copy using whatever +license you like. + +*/ + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + + + + + +#ifndef JSON_FORWARD_AMALGATED_H_INCLUDED +# define JSON_FORWARD_AMALGATED_H_INCLUDED +/// If defined, indicates that the source file is amalgated +/// to prevent private header inclusion. +#define JSON_IS_AMALGAMATION + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/config.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_CONFIG_H_INCLUDED +#define JSON_CONFIG_H_INCLUDED + +/// If defined, indicates that json library is embedded in CppTL library. +//# define JSON_IN_CPPTL 1 + +/// If defined, indicates that json may leverage CppTL library +//# define JSON_USE_CPPTL 1 +/// If defined, indicates that cpptl vector based map should be used instead of +/// std::map +/// as Value container. +//# define JSON_USE_CPPTL_SMALLMAP 1 + +// If non-zero, the library uses exceptions to report bad input instead of C +// assertion macros. The default is to use exceptions. +#ifndef JSON_USE_EXCEPTION +#define JSON_USE_EXCEPTION 1 +#endif + +/// If defined, indicates that the source file is amalgated +/// to prevent private header inclusion. +/// Remarks: it is automatically defined in the generated amalgated header. +// #define JSON_IS_AMALGAMATION + +#ifdef JSON_IN_CPPTL +#include +#ifndef JSON_USE_CPPTL +#define JSON_USE_CPPTL 1 +#endif +#endif + +#ifdef JSON_IN_CPPTL +#define JSON_API CPPTL_API +#elif defined(JSON_DLL_BUILD) +#if defined(_MSC_VER) +#define JSON_API __declspec(dllexport) +#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING +#endif // if defined(_MSC_VER) +#elif defined(JSON_DLL) +#if defined(_MSC_VER) +#define JSON_API __declspec(dllimport) +#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING +#endif // if defined(_MSC_VER) +#endif // ifdef JSON_IN_CPPTL +#if !defined(JSON_API) +#define JSON_API +#endif + +// If JSON_NO_INT64 is defined, then Json only support C++ "int" type for +// integer +// Storages, and 64 bits integer support is disabled. +// #define JSON_NO_INT64 1 + +#if defined(_MSC_VER) // MSVC +# if _MSC_VER <= 1200 // MSVC 6 + // Microsoft Visual Studio 6 only support conversion from __int64 to double + // (no conversion from unsigned __int64). +# define JSON_USE_INT64_DOUBLE_CONVERSION 1 + // Disable warning 4786 for VS6 caused by STL (identifier was truncated to '255' + // characters in the debug information) + // All projects I've ever seen with VS6 were using this globally (not bothering + // with pragma push/pop). +# pragma warning(disable : 4786) +# endif // MSVC 6 + +# if _MSC_VER >= 1500 // MSVC 2008 + /// Indicates that the following function is deprecated. +# define JSONCPP_DEPRECATED(message) __declspec(deprecated(message)) +# endif + +#endif // defined(_MSC_VER) + + +#ifndef JSON_HAS_RVALUE_REFERENCES + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // MSVC >= 2010 +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // MSVC >= 2010 + +#ifdef __clang__ +#if __has_feature(cxx_rvalue_references) +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // has_feature + +#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc) +#if defined(__GXX_EXPERIMENTAL_CXX0X__) || (__cplusplus >= 201103L) +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // GXX_EXPERIMENTAL + +#endif // __clang__ || __GNUC__ + +#endif // not defined JSON_HAS_RVALUE_REFERENCES + +#ifndef JSON_HAS_RVALUE_REFERENCES +#define JSON_HAS_RVALUE_REFERENCES 0 +#endif + +#ifdef __clang__ +#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc) +# if (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)) +# define JSONCPP_DEPRECATED(message) __attribute__ ((deprecated(message))) +# elif (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1)) +# define JSONCPP_DEPRECATED(message) __attribute__((__deprecated__)) +# endif // GNUC version +#endif // __clang__ || __GNUC__ + +#if !defined(JSONCPP_DEPRECATED) +#define JSONCPP_DEPRECATED(message) +#endif // if !defined(JSONCPP_DEPRECATED) + +namespace Json { +typedef int Int; +typedef unsigned int UInt; +#if defined(JSON_NO_INT64) +typedef int LargestInt; +typedef unsigned int LargestUInt; +#undef JSON_HAS_INT64 +#else // if defined(JSON_NO_INT64) +// For Microsoft Visual use specific types as long long is not supported +#if defined(_MSC_VER) // Microsoft Visual Studio +typedef __int64 Int64; +typedef unsigned __int64 UInt64; +#else // if defined(_MSC_VER) // Other platforms, use long long +typedef long long int Int64; +typedef unsigned long long int UInt64; +#endif // if defined(_MSC_VER) +typedef Int64 LargestInt; +typedef UInt64 LargestUInt; +#define JSON_HAS_INT64 +#endif // if defined(JSON_NO_INT64) +} // end namespace Json + +#endif // JSON_CONFIG_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/config.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/forwards.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_FORWARDS_H_INCLUDED +#define JSON_FORWARDS_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "config.h" +#endif // if !defined(JSON_IS_AMALGAMATION) + +namespace Json { + +// writer.h +class FastWriter; +class StyledWriter; + +// reader.h +class Reader; + +// features.h +class Features; + +// value.h +typedef unsigned int ArrayIndex; +class StaticString; +class Path; +class PathArgument; +class Value; +class ValueIteratorBase; +class ValueIterator; +class ValueConstIterator; + +} // namespace Json + +#endif // JSON_FORWARDS_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/forwards.h +// ////////////////////////////////////////////////////////////////////// + + + + + +#endif //ifndef JSON_FORWARD_AMALGATED_H_INCLUDED diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h new file mode 100755 index 000000000..01225cac6 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h @@ -0,0 +1,2075 @@ +/// Json-cpp amalgated header (http://jsoncpp.sourceforge.net/). +/// It is intended to be used with #include "json/json.h" + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + +/* +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... + +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. + +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is +released under the terms of the MIT License (see below). + +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. + +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: + + http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +======================================================================== +Copyright (c) 2007-2010 Baptiste Lepilleur + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +======================================================================== +(END LICENSE TEXT) + +The MIT license is compatible with both the GPL and commercial +software, affording one all of the rights of Public Domain with the +minor nuisance of being required to keep the above copyright notice +and license text in the source code. Note also that by accepting the +Public Domain "license" you can re-license your copy using whatever +license you like. + +*/ + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + + + + + +#ifndef JSON_AMALGATED_H_INCLUDED +# define JSON_AMALGATED_H_INCLUDED +/// If defined, indicates that the source file is amalgated +/// to prevent private header inclusion. +#define JSON_IS_AMALGAMATION + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/version.h +// ////////////////////////////////////////////////////////////////////// + +// DO NOT EDIT. This file (and "version") is generated by CMake. +// Run CMake configure step to update it. +#ifndef JSON_VERSION_H_INCLUDED +# define JSON_VERSION_H_INCLUDED + +# define JSONCPP_VERSION_STRING "1.6.5" +# define JSONCPP_VERSION_MAJOR 1 +# define JSONCPP_VERSION_MINOR 6 +# define JSONCPP_VERSION_PATCH 5 +# define JSONCPP_VERSION_QUALIFIER +# define JSONCPP_VERSION_HEXA ((JSONCPP_VERSION_MAJOR << 24) | (JSONCPP_VERSION_MINOR << 16) | (JSONCPP_VERSION_PATCH << 8)) + +#endif // JSON_VERSION_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/version.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/config.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_CONFIG_H_INCLUDED +#define JSON_CONFIG_H_INCLUDED + +/// If defined, indicates that json library is embedded in CppTL library. +//# define JSON_IN_CPPTL 1 + +/// If defined, indicates that json may leverage CppTL library +//# define JSON_USE_CPPTL 1 +/// If defined, indicates that cpptl vector based map should be used instead of +/// std::map +/// as Value container. +//# define JSON_USE_CPPTL_SMALLMAP 1 + +// If non-zero, the library uses exceptions to report bad input instead of C +// assertion macros. The default is to use exceptions. +#ifndef JSON_USE_EXCEPTION +#define JSON_USE_EXCEPTION 1 +#endif + +/// If defined, indicates that the source file is amalgated +/// to prevent private header inclusion. +/// Remarks: it is automatically defined in the generated amalgated header. +// #define JSON_IS_AMALGAMATION + +#ifdef JSON_IN_CPPTL +#include +#ifndef JSON_USE_CPPTL +#define JSON_USE_CPPTL 1 +#endif +#endif + +#ifdef JSON_IN_CPPTL +#define JSON_API CPPTL_API +#elif defined(JSON_DLL_BUILD) +#if defined(_MSC_VER) +#define JSON_API __declspec(dllexport) +#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING +#endif // if defined(_MSC_VER) +#elif defined(JSON_DLL) +#if defined(_MSC_VER) +#define JSON_API __declspec(dllimport) +#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING +#endif // if defined(_MSC_VER) +#endif // ifdef JSON_IN_CPPTL +#if !defined(JSON_API) +#define JSON_API +#endif + +// If JSON_NO_INT64 is defined, then Json only support C++ "int" type for +// integer +// Storages, and 64 bits integer support is disabled. +// #define JSON_NO_INT64 1 + +#if defined(_MSC_VER) // MSVC +# if _MSC_VER <= 1200 // MSVC 6 + // Microsoft Visual Studio 6 only support conversion from __int64 to double + // (no conversion from unsigned __int64). +# define JSON_USE_INT64_DOUBLE_CONVERSION 1 + // Disable warning 4786 for VS6 caused by STL (identifier was truncated to '255' + // characters in the debug information) + // All projects I've ever seen with VS6 were using this globally (not bothering + // with pragma push/pop). +# pragma warning(disable : 4786) +# endif // MSVC 6 + +# if _MSC_VER >= 1500 // MSVC 2008 + /// Indicates that the following function is deprecated. +# define JSONCPP_DEPRECATED(message) __declspec(deprecated(message)) +# endif + +#endif // defined(_MSC_VER) + + +#ifndef JSON_HAS_RVALUE_REFERENCES + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // MSVC >= 2010 +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // MSVC >= 2010 + +#ifdef __clang__ +#if __has_feature(cxx_rvalue_references) +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // has_feature + +#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc) +#if defined(__GXX_EXPERIMENTAL_CXX0X__) || (__cplusplus >= 201103L) +#define JSON_HAS_RVALUE_REFERENCES 1 +#endif // GXX_EXPERIMENTAL + +#endif // __clang__ || __GNUC__ + +#endif // not defined JSON_HAS_RVALUE_REFERENCES + +#ifndef JSON_HAS_RVALUE_REFERENCES +#define JSON_HAS_RVALUE_REFERENCES 0 +#endif + +#ifdef __clang__ +#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc) +# if (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)) +# define JSONCPP_DEPRECATED(message) __attribute__ ((deprecated(message))) +# elif (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1)) +# define JSONCPP_DEPRECATED(message) __attribute__((__deprecated__)) +# endif // GNUC version +#endif // __clang__ || __GNUC__ + +#if !defined(JSONCPP_DEPRECATED) +#define JSONCPP_DEPRECATED(message) +#endif // if !defined(JSONCPP_DEPRECATED) + +namespace Json { +typedef int Int; +typedef unsigned int UInt; +#if defined(JSON_NO_INT64) +typedef int LargestInt; +typedef unsigned int LargestUInt; +#undef JSON_HAS_INT64 +#else // if defined(JSON_NO_INT64) +// For Microsoft Visual use specific types as long long is not supported +#if defined(_MSC_VER) // Microsoft Visual Studio +typedef __int64 Int64; +typedef unsigned __int64 UInt64; +#else // if defined(_MSC_VER) // Other platforms, use long long +typedef long long int Int64; +typedef unsigned long long int UInt64; +#endif // if defined(_MSC_VER) +typedef Int64 LargestInt; +typedef UInt64 LargestUInt; +#define JSON_HAS_INT64 +#endif // if defined(JSON_NO_INT64) +} // end namespace Json + +#endif // JSON_CONFIG_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/config.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/forwards.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_FORWARDS_H_INCLUDED +#define JSON_FORWARDS_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "config.h" +#endif // if !defined(JSON_IS_AMALGAMATION) + +namespace Json { + +// writer.h +class FastWriter; +class StyledWriter; + +// reader.h +class Reader; + +// features.h +class Features; + +// value.h +typedef unsigned int ArrayIndex; +class StaticString; +class Path; +class PathArgument; +class Value; +class ValueIteratorBase; +class ValueIterator; +class ValueConstIterator; + +} // namespace Json + +#endif // JSON_FORWARDS_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/forwards.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/features.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_FEATURES_H_INCLUDED +#define CPPTL_JSON_FEATURES_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "forwards.h" +#endif // if !defined(JSON_IS_AMALGAMATION) + +namespace Json { + +/** \brief Configuration passed to reader and writer. + * This configuration object can be used to force the Reader or Writer + * to behave in a standard conforming way. + */ +class JSON_API Features { +public: + /** \brief A configuration that allows all features and assumes all strings + * are UTF-8. + * - C & C++ comments are allowed + * - Root object can be any JSON value + * - Assumes Value strings are encoded in UTF-8 + */ + static Features all(); + + /** \brief A configuration that is strictly compatible with the JSON + * specification. + * - Comments are forbidden. + * - Root object must be either an array or an object value. + * - Assumes Value strings are encoded in UTF-8 + */ + static Features strictMode(); + + /** \brief Initialize the configuration like JsonConfig::allFeatures; + */ + Features(); + + /// \c true if comments are allowed. Default: \c true. + bool allowComments_; + + /// \c true if root must be either an array or an object value. Default: \c + /// false. + bool strictRoot_; + + /// \c true if dropped null placeholders are allowed. Default: \c false. + bool allowDroppedNullPlaceholders_; + + /// \c true if numeric object key are allowed. Default: \c false. + bool allowNumericKeys_; +}; + +} // namespace Json + +#endif // CPPTL_JSON_FEATURES_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/features.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/value.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_H_INCLUDED +#define CPPTL_JSON_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "forwards.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include + +#ifndef JSON_USE_CPPTL_SMALLMAP +#include +#else +#include +#endif +#ifdef JSON_USE_CPPTL +#include +#endif + +// Disable warning C4251: : needs to have dll-interface to +// be used by... +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +/** \brief JSON (JavaScript Object Notation). + */ +namespace Json { + +/** Base class for all exceptions we throw. + * + * We use nothing but these internally. Of course, STL can throw others. + */ +class JSON_API Exception : public std::exception { +public: + Exception(std::string const& msg); + ~Exception() throw() ; + char const* what() const throw() ; +protected: + std::string msg_; +}; + +/** Exceptions which the user cannot easily avoid. + * + * E.g. out-of-memory (when we use malloc), stack-overflow, malicious input + * + * \remark derived from Json::Exception + */ +class JSON_API RuntimeError : public Exception { +public: + RuntimeError(std::string const& msg); +}; + +/** Exceptions thrown by JSON_ASSERT/JSON_FAIL macros. + * + * These are precondition-violations (user bugs) and internal errors (our bugs). + * + * \remark derived from Json::Exception + */ +class JSON_API LogicError : public Exception { +public: + LogicError(std::string const& msg); +}; + +/// used internally +void throwRuntimeError(std::string const& msg); +/// used internally +void throwLogicError(std::string const& msg); + +/** \brief Type of the value held by a Value object. + */ +enum ValueType { + nullValue = 0, ///< 'null' value + intValue, ///< signed integer value + uintValue, ///< unsigned integer value + realValue, ///< double value + stringValue, ///< UTF-8 string value + booleanValue, ///< bool value + arrayValue, ///< array value (ordered list) + objectValue ///< object value (collection of name/value pairs). +}; + +enum CommentPlacement { + commentBefore = 0, ///< a comment placed on the line before a value + commentAfterOnSameLine, ///< a comment just after a value on the same line + commentAfter, ///< a comment on the line after a value (only make sense for + /// root value) + numberOfCommentPlacement +}; + +//# ifdef JSON_USE_CPPTL +// typedef CppTL::AnyEnumerator EnumMemberNames; +// typedef CppTL::AnyEnumerator EnumValues; +//# endif + +/** \brief Lightweight wrapper to tag static string. + * + * Value constructor and objectValue member assignement takes advantage of the + * StaticString and avoid the cost of string duplication when storing the + * string or the member name. + * + * Example of usage: + * \code + * Json::Value aValue( StaticString("some text") ); + * Json::Value object; + * static const StaticString code("code"); + * object[code] = 1234; + * \endcode + */ +class JSON_API StaticString { +public: + explicit StaticString(const char* czstring) : c_str_(czstring) {} + + operator const char*() const { return c_str_; } + + const char* c_str() const { return c_str_; } + +private: + const char* c_str_; +}; + +/** \brief Represents a JSON value. + * + * This class is a discriminated union wrapper that can represents a: + * - signed integer [range: Value::minInt - Value::maxInt] + * - unsigned integer (range: 0 - Value::maxUInt) + * - double + * - UTF-8 string + * - boolean + * - 'null' + * - an ordered list of Value + * - collection of name/value pairs (javascript object) + * + * The type of the held value is represented by a #ValueType and + * can be obtained using type(). + * + * Values of an #objectValue or #arrayValue can be accessed using operator[]() + * methods. + * Non-const methods will automatically create the a #nullValue element + * if it does not exist. + * The sequence of an #arrayValue will be automatically resized and initialized + * with #nullValue. resize() can be used to enlarge or truncate an #arrayValue. + * + * The get() methods can be used to obtain default value in the case the + * required element does not exist. + * + * It is possible to iterate over the list of a #objectValue values using + * the getMemberNames() method. + * + * \note #Value string-length fit in size_t, but keys must be < 2^30. + * (The reason is an implementation detail.) A #CharReader will raise an + * exception if a bound is exceeded to avoid security holes in your app, + * but the Value API does *not* check bounds. That is the responsibility + * of the caller. + */ +class JSON_API Value { + friend class ValueIteratorBase; +public: + typedef std::vector Members; + typedef ValueIterator iterator; + typedef ValueConstIterator const_iterator; + typedef Json::UInt UInt; + typedef Json::Int Int; +#if defined(JSON_HAS_INT64) + typedef Json::UInt64 UInt64; + typedef Json::Int64 Int64; +#endif // defined(JSON_HAS_INT64) + typedef Json::LargestInt LargestInt; + typedef Json::LargestUInt LargestUInt; + typedef Json::ArrayIndex ArrayIndex; + + static const Value& null; ///< We regret this reference to a global instance; prefer the simpler Value(). + static const Value& nullRef; ///< just a kludge for binary-compatibility; same as null + /// Minimum signed integer value that can be stored in a Json::Value. + static const LargestInt minLargestInt; + /// Maximum signed integer value that can be stored in a Json::Value. + static const LargestInt maxLargestInt; + /// Maximum unsigned integer value that can be stored in a Json::Value. + static const LargestUInt maxLargestUInt; + + /// Minimum signed int value that can be stored in a Json::Value. + static const Int minInt; + /// Maximum signed int value that can be stored in a Json::Value. + static const Int maxInt; + /// Maximum unsigned int value that can be stored in a Json::Value. + static const UInt maxUInt; + +#if defined(JSON_HAS_INT64) + /// Minimum signed 64 bits int value that can be stored in a Json::Value. + static const Int64 minInt64; + /// Maximum signed 64 bits int value that can be stored in a Json::Value. + static const Int64 maxInt64; + /// Maximum unsigned 64 bits int value that can be stored in a Json::Value. + static const UInt64 maxUInt64; +#endif // defined(JSON_HAS_INT64) + +private: +#ifndef JSONCPP_DOC_EXCLUDE_IMPLEMENTATION + class CZString { + public: + enum DuplicationPolicy { + noDuplication = 0, + duplicate, + duplicateOnCopy + }; + CZString(ArrayIndex index); + CZString(char const* str, unsigned length, DuplicationPolicy allocate); + CZString(CZString const& other); +#if JSON_HAS_RVALUE_REFERENCES + CZString(CZString&& other); +#endif + ~CZString(); + CZString& operator=(CZString other); + bool operator<(CZString const& other) const; + bool operator==(CZString const& other) const; + ArrayIndex index() const; + //const char* c_str() const; ///< \deprecated + char const* data() const; + unsigned length() const; + bool isStaticString() const; + + private: + void swap(CZString& other); + + struct StringStorage { + unsigned policy_: 2; + unsigned length_: 30; // 1GB max + }; + + char const* cstr_; // actually, a prefixed string, unless policy is noDup + union { + ArrayIndex index_; + StringStorage storage_; + }; + }; + +public: +#ifndef JSON_USE_CPPTL_SMALLMAP + typedef std::map ObjectValues; +#else + typedef CppTL::SmallMap ObjectValues; +#endif // ifndef JSON_USE_CPPTL_SMALLMAP +#endif // ifndef JSONCPP_DOC_EXCLUDE_IMPLEMENTATION + +public: + /** \brief Create a default Value of the given type. + + This is a very useful constructor. + To create an empty array, pass arrayValue. + To create an empty object, pass objectValue. + Another Value can then be set to this one by assignment. +This is useful since clear() and resize() will not alter types. + + Examples: +\code +Json::Value null_value; // null +Json::Value arr_value(Json::arrayValue); // [] +Json::Value obj_value(Json::objectValue); // {} +\endcode + */ + Value(ValueType type = nullValue); + Value(Int value); + Value(UInt value); +#if defined(JSON_HAS_INT64) + Value(Int64 value); + Value(UInt64 value); +#endif // if defined(JSON_HAS_INT64) + Value(double value); + Value(const char* value); ///< Copy til first 0. (NULL causes to seg-fault.) + Value(const char* begin, const char* end); ///< Copy all, incl zeroes. + /** \brief Constructs a value from a static string. + + * Like other value string constructor but do not duplicate the string for + * internal storage. The given string must remain alive after the call to this + * constructor. + * \note This works only for null-terminated strings. (We cannot change the + * size of this class, so we have nowhere to store the length, + * which might be computed later for various operations.) + * + * Example of usage: + * \code + * static StaticString foo("some text"); + * Json::Value aValue(foo); + * \endcode + */ + Value(const StaticString& value); + Value(const std::string& value); ///< Copy data() til size(). Embedded zeroes too. +#ifdef JSON_USE_CPPTL + Value(const CppTL::ConstString& value); +#endif + Value(bool value); + /// Deep copy. + Value(const Value& other); +#if JSON_HAS_RVALUE_REFERENCES + /// Move constructor + Value(Value&& other); +#endif + ~Value(); + + /// Deep copy, then swap(other). + /// \note Over-write existing comments. To preserve comments, use #swapPayload(). + Value& operator=(Value other); + /// Swap everything. + void swap(Value& other); + /// Swap values but leave comments and source offsets in place. + void swapPayload(Value& other); + + ValueType type() const; + + /// Compare payload only, not comments etc. + bool operator<(const Value& other) const; + bool operator<=(const Value& other) const; + bool operator>=(const Value& other) const; + bool operator>(const Value& other) const; + bool operator==(const Value& other) const; + bool operator!=(const Value& other) const; + int compare(const Value& other) const; + + const char* asCString() const; ///< Embedded zeroes could cause you trouble! + std::string asString() const; ///< Embedded zeroes are possible. + /** Get raw char* of string-value. + * \return false if !string. (Seg-fault if str or end are NULL.) + */ + bool getString( + char const** begin, char const** end) const; +#ifdef JSON_USE_CPPTL + CppTL::ConstString asConstString() const; +#endif + Int asInt() const; + UInt asUInt() const; +#if defined(JSON_HAS_INT64) + Int64 asInt64() const; + UInt64 asUInt64() const; +#endif // if defined(JSON_HAS_INT64) + LargestInt asLargestInt() const; + LargestUInt asLargestUInt() const; + float asFloat() const; + double asDouble() const; + bool asBool() const; + + bool isNull() const; + bool isBool() const; + bool isInt() const; + bool isInt64() const; + bool isUInt() const; + bool isUInt64() const; + bool isIntegral() const; + bool isDouble() const; + bool isNumeric() const; + bool isString() const; + bool isArray() const; + bool isObject() const; + + bool isConvertibleTo(ValueType other) const; + + /// Number of values in array or object + ArrayIndex size() const; + + /// \brief Return true if empty array, empty object, or null; + /// otherwise, false. + bool empty() const; + + /// Return isNull() + bool operator!() const; + + /// Remove all object members and array elements. + /// \pre type() is arrayValue, objectValue, or nullValue + /// \post type() is unchanged + void clear(); + + /// Resize the array to size elements. + /// New elements are initialized to null. + /// May only be called on nullValue or arrayValue. + /// \pre type() is arrayValue or nullValue + /// \post type() is arrayValue + void resize(ArrayIndex size); + + /// Access an array element (zero based index ). + /// If the array contains less than index element, then null value are + /// inserted + /// in the array so that its size is index+1. + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + Value& operator[](ArrayIndex index); + + /// Access an array element (zero based index ). + /// If the array contains less than index element, then null value are + /// inserted + /// in the array so that its size is index+1. + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + Value& operator[](int index); + + /// Access an array element (zero based index ) + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + const Value& operator[](ArrayIndex index) const; + + /// Access an array element (zero based index ) + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + const Value& operator[](int index) const; + + /// If the array contains at least index+1 elements, returns the element + /// value, + /// otherwise returns defaultValue. + Value get(ArrayIndex index, const Value& defaultValue) const; + /// Return true if index < size(). + bool isValidIndex(ArrayIndex index) const; + /// \brief Append value to array at the end. + /// + /// Equivalent to jsonvalue[jsonvalue.size()] = value; + Value& append(const Value& value); + + /// Access an object value by name, create a null member if it does not exist. + /// \note Because of our implementation, keys are limited to 2^30 -1 chars. + /// Exceeding that will cause an exception. + Value& operator[](const char* key); + /// Access an object value by name, returns null if there is no member with + /// that name. + const Value& operator[](const char* key) const; + /// Access an object value by name, create a null member if it does not exist. + /// \param key may contain embedded nulls. + Value& operator[](const std::string& key); + /// Access an object value by name, returns null if there is no member with + /// that name. + /// \param key may contain embedded nulls. + const Value& operator[](const std::string& key) const; + /** \brief Access an object value by name, create a null member if it does not + exist. + + * If the object has no entry for that name, then the member name used to store + * the new entry is not duplicated. + * Example of use: + * \code + * Json::Value object; + * static const StaticString code("code"); + * object[code] = 1234; + * \endcode + */ + Value& operator[](const StaticString& key); +#ifdef JSON_USE_CPPTL + /// Access an object value by name, create a null member if it does not exist. + Value& operator[](const CppTL::ConstString& key); + /// Access an object value by name, returns null if there is no member with + /// that name. + const Value& operator[](const CppTL::ConstString& key) const; +#endif + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + Value get(const char* key, const Value& defaultValue) const; + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + /// \note key may contain embedded nulls. + Value get(const char* begin, const char* end, const Value& defaultValue) const; + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + /// \param key may contain embedded nulls. + Value get(const std::string& key, const Value& defaultValue) const; +#ifdef JSON_USE_CPPTL + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + Value get(const CppTL::ConstString& key, const Value& defaultValue) const; +#endif + /// Most general and efficient version of isMember()const, get()const, + /// and operator[]const + /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 + Value const* find(char const* begin, char const* end) const; + /// Most general and efficient version of object-mutators. + /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 + /// \return non-zero, but JSON_ASSERT if this is neither object nor nullValue. + Value const* demand(char const* begin, char const* end); + /// \brief Remove and return the named member. + /// + /// Do nothing if it did not exist. + /// \return the removed Value, or null. + /// \pre type() is objectValue or nullValue + /// \post type() is unchanged + /// \deprecated + Value removeMember(const char* key); + /// Same as removeMember(const char*) + /// \param key may contain embedded nulls. + /// \deprecated + Value removeMember(const std::string& key); + /// Same as removeMember(const char* begin, const char* end, Value* removed), + /// but 'key' is null-terminated. + bool removeMember(const char* key, Value* removed); + /** \brief Remove the named map member. + + Update 'removed' iff removed. + \param key may contain embedded nulls. + \return true iff removed (no exceptions) + */ + bool removeMember(std::string const& key, Value* removed); + /// Same as removeMember(std::string const& key, Value* removed) + bool removeMember(const char* begin, const char* end, Value* removed); + /** \brief Remove the indexed array element. + + O(n) expensive operations. + Update 'removed' iff removed. + \return true iff removed (no exceptions) + */ + bool removeIndex(ArrayIndex i, Value* removed); + + /// Return true if the object has a member named key. + /// \note 'key' must be null-terminated. + bool isMember(const char* key) const; + /// Return true if the object has a member named key. + /// \param key may contain embedded nulls. + bool isMember(const std::string& key) const; + /// Same as isMember(std::string const& key)const + bool isMember(const char* begin, const char* end) const; +#ifdef JSON_USE_CPPTL + /// Return true if the object has a member named key. + bool isMember(const CppTL::ConstString& key) const; +#endif + + /// \brief Return a list of the member names. + /// + /// If null, return an empty list. + /// \pre type() is objectValue or nullValue + /// \post if type() was nullValue, it remains nullValue + Members getMemberNames() const; + + //# ifdef JSON_USE_CPPTL + // EnumMemberNames enumMemberNames() const; + // EnumValues enumValues() const; + //# endif + + /// \deprecated Always pass len. + JSONCPP_DEPRECATED("Use setComment(std::string const&) instead.") + void setComment(const char* comment, CommentPlacement placement); + /// Comments must be //... or /* ... */ + void setComment(const char* comment, size_t len, CommentPlacement placement); + /// Comments must be //... or /* ... */ + void setComment(const std::string& comment, CommentPlacement placement); + bool hasComment(CommentPlacement placement) const; + /// Include delimiters and embedded newlines. + std::string getComment(CommentPlacement placement) const; + + std::string toStyledString() const; + + const_iterator begin() const; + const_iterator end() const; + + iterator begin(); + iterator end(); + + // Accessors for the [start, limit) range of bytes within the JSON text from + // which this value was parsed, if any. + void setOffsetStart(size_t start); + void setOffsetLimit(size_t limit); + size_t getOffsetStart() const; + size_t getOffsetLimit() const; + +private: + void initBasic(ValueType type, bool allocated = false); + + Value& resolveReference(const char* key); + Value& resolveReference(const char* key, const char* end); + + struct CommentInfo { + CommentInfo(); + ~CommentInfo(); + + void setComment(const char* text, size_t len); + + char* comment_; + }; + + // struct MemberNamesTransform + //{ + // typedef const char *result_type; + // const char *operator()( const CZString &name ) const + // { + // return name.c_str(); + // } + //}; + + union ValueHolder { + LargestInt int_; + LargestUInt uint_; + double real_; + bool bool_; + char* string_; // actually ptr to unsigned, followed by str, unless !allocated_ + ObjectValues* map_; + } value_; + ValueType type_ : 8; + unsigned int allocated_ : 1; // Notes: if declared as bool, bitfield is useless. + // If not allocated_, string_ must be null-terminated. + CommentInfo* comments_; + + // [start, limit) byte offsets in the source JSON text from which this Value + // was extracted. + size_t start_; + size_t limit_; +}; + +/** \brief Experimental and untested: represents an element of the "path" to + * access a node. + */ +class JSON_API PathArgument { +public: + friend class Path; + + PathArgument(); + PathArgument(ArrayIndex index); + PathArgument(const char* key); + PathArgument(const std::string& key); + +private: + enum Kind { + kindNone = 0, + kindIndex, + kindKey + }; + std::string key_; + ArrayIndex index_; + Kind kind_; +}; + +/** \brief Experimental and untested: represents a "path" to access a node. + * + * Syntax: + * - "." => root node + * - ".[n]" => elements at index 'n' of root node (an array value) + * - ".name" => member named 'name' of root node (an object value) + * - ".name1.name2.name3" + * - ".[0][1][2].name1[3]" + * - ".%" => member name is provided as parameter + * - ".[%]" => index is provied as parameter + */ +class JSON_API Path { +public: + Path(const std::string& path, + const PathArgument& a1 = PathArgument(), + const PathArgument& a2 = PathArgument(), + const PathArgument& a3 = PathArgument(), + const PathArgument& a4 = PathArgument(), + const PathArgument& a5 = PathArgument()); + + const Value& resolve(const Value& root) const; + Value resolve(const Value& root, const Value& defaultValue) const; + /// Creates the "path" to access the specified node and returns a reference on + /// the node. + Value& make(Value& root) const; + +private: + typedef std::vector InArgs; + typedef std::vector Args; + + void makePath(const std::string& path, const InArgs& in); + void addPathInArg(const std::string& path, + const InArgs& in, + InArgs::const_iterator& itInArg, + PathArgument::Kind kind); + void invalidPath(const std::string& path, int location); + + Args args_; +}; + +/** \brief base class for Value iterators. + * + */ +class JSON_API ValueIteratorBase { +public: + typedef std::bidirectional_iterator_tag iterator_category; + typedef unsigned int size_t; + typedef int difference_type; + typedef ValueIteratorBase SelfType; + + bool operator==(const SelfType& other) const { return isEqual(other); } + + bool operator!=(const SelfType& other) const { return !isEqual(other); } + + difference_type operator-(const SelfType& other) const { + return other.computeDistance(*this); + } + + /// Return either the index or the member name of the referenced value as a + /// Value. + Value key() const; + + /// Return the index of the referenced Value, or -1 if it is not an arrayValue. + UInt index() const; + + /// Return the member name of the referenced Value, or "" if it is not an + /// objectValue. + /// \note Avoid `c_str()` on result, as embedded zeroes are possible. + std::string name() const; + + /// Return the member name of the referenced Value. "" if it is not an + /// objectValue. + /// \deprecated This cannot be used for UTF-8 strings, since there can be embedded nulls. + JSONCPP_DEPRECATED("Use `key = name();` instead.") + char const* memberName() const; + /// Return the member name of the referenced Value, or NULL if it is not an + /// objectValue. + /// \note Better version than memberName(). Allows embedded nulls. + char const* memberName(char const** end) const; + +protected: + Value& deref() const; + + void increment(); + + void decrement(); + + difference_type computeDistance(const SelfType& other) const; + + bool isEqual(const SelfType& other) const; + + void copy(const SelfType& other); + +private: + Value::ObjectValues::iterator current_; + // Indicates that iterator is for a null value. + bool isNull_; + +public: + // For some reason, BORLAND needs these at the end, rather + // than earlier. No idea why. + ValueIteratorBase(); + explicit ValueIteratorBase(const Value::ObjectValues::iterator& current); +}; + +/** \brief const iterator for object and array value. + * + */ +class JSON_API ValueConstIterator : public ValueIteratorBase { + friend class Value; + +public: + typedef const Value value_type; + //typedef unsigned int size_t; + //typedef int difference_type; + typedef const Value& reference; + typedef const Value* pointer; + typedef ValueConstIterator SelfType; + + ValueConstIterator(); + ValueConstIterator(ValueIterator const& other); + +private: +/*! \internal Use by Value to create an iterator. + */ + explicit ValueConstIterator(const Value::ObjectValues::iterator& current); +public: + SelfType& operator=(const ValueIteratorBase& other); + + SelfType operator++(int) { + SelfType temp(*this); + ++*this; + return temp; + } + + SelfType operator--(int) { + SelfType temp(*this); + --*this; + return temp; + } + + SelfType& operator--() { + decrement(); + return *this; + } + + SelfType& operator++() { + increment(); + return *this; + } + + reference operator*() const { return deref(); } + + pointer operator->() const { return &deref(); } +}; + +/** \brief Iterator for object and array value. + */ +class JSON_API ValueIterator : public ValueIteratorBase { + friend class Value; + +public: + typedef Value value_type; + typedef unsigned int size_t; + typedef int difference_type; + typedef Value& reference; + typedef Value* pointer; + typedef ValueIterator SelfType; + + ValueIterator(); + explicit ValueIterator(const ValueConstIterator& other); + ValueIterator(const ValueIterator& other); + +private: +/*! \internal Use by Value to create an iterator. + */ + explicit ValueIterator(const Value::ObjectValues::iterator& current); +public: + SelfType& operator=(const SelfType& other); + + SelfType operator++(int) { + SelfType temp(*this); + ++*this; + return temp; + } + + SelfType operator--(int) { + SelfType temp(*this); + --*this; + return temp; + } + + SelfType& operator--() { + decrement(); + return *this; + } + + SelfType& operator++() { + increment(); + return *this; + } + + reference operator*() const { return deref(); } + + pointer operator->() const { return &deref(); } +}; + +} // namespace Json + + +namespace std { +/// Specialize std::swap() for Json::Value. +template<> +inline void swap(Json::Value& a, Json::Value& b) { a.swap(b); } +} + + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // CPPTL_JSON_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/value.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/reader.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_READER_H_INCLUDED +#define CPPTL_JSON_READER_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "features.h" +#include "value.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include + +// Disable warning C4251: : needs to have dll-interface to +// be used by... +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +namespace Json { + +/** \brief Unserialize a JSON document into a + *Value. + * + * \deprecated Use CharReader and CharReaderBuilder. + */ +class JSON_API Reader { +public: + typedef char Char; + typedef const Char* Location; + + /** \brief An error tagged with where in the JSON text it was encountered. + * + * The offsets give the [start, limit) range of bytes within the text. Note + * that this is bytes, not codepoints. + * + */ + struct StructuredError { + size_t offset_start; + size_t offset_limit; + std::string message; + }; + + /** \brief Constructs a Reader allowing all features + * for parsing. + */ + Reader(); + + /** \brief Constructs a Reader allowing the specified feature set + * for parsing. + */ + Reader(const Features& features); + + /** \brief Read a Value from a JSON + * document. + * \param document UTF-8 encoded string containing the document to read. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param collectComments \c true to collect comment and allow writing them + * back during + * serialization, \c false to discard comments. + * This parameter is ignored if + * Features::allowComments_ + * is \c false. + * \return \c true if the document was successfully parsed, \c false if an + * error occurred. + */ + bool + parse(const std::string& document, Value& root, bool collectComments = true); + + /** \brief Read a Value from a JSON + document. + * \param beginDoc Pointer on the beginning of the UTF-8 encoded string of the + document to read. + * \param endDoc Pointer on the end of the UTF-8 encoded string of the + document to read. + * Must be >= beginDoc. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param collectComments \c true to collect comment and allow writing them + back during + * serialization, \c false to discard comments. + * This parameter is ignored if + Features::allowComments_ + * is \c false. + * \return \c true if the document was successfully parsed, \c false if an + error occurred. + */ + bool parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments = true); + + /// \brief Parse from input stream. + /// \see Json::operator>>(std::istream&, Json::Value&). + bool parse(std::istream& is, Value& root, bool collectComments = true); + + /** \brief Returns a user friendly string that list errors in the parsed + * document. + * \return Formatted error message with the list of errors with their location + * in + * the parsed document. An empty string is returned if no error + * occurred + * during parsing. + * \deprecated Use getFormattedErrorMessages() instead (typo fix). + */ + JSONCPP_DEPRECATED("Use getFormattedErrorMessages() instead.") + std::string getFormatedErrorMessages() const; + + /** \brief Returns a user friendly string that list errors in the parsed + * document. + * \return Formatted error message with the list of errors with their location + * in + * the parsed document. An empty string is returned if no error + * occurred + * during parsing. + */ + std::string getFormattedErrorMessages() const; + + /** \brief Returns a vector of structured erros encounted while parsing. + * \return A (possibly empty) vector of StructuredError objects. Currently + * only one error can be returned, but the caller should tolerate + * multiple + * errors. This can occur if the parser recovers from a non-fatal + * parse error and then encounters additional errors. + */ + std::vector getStructuredErrors() const; + + /** \brief Add a semantic error message. + * \param value JSON Value location associated with the error + * \param message The error message. + * \return \c true if the error was successfully added, \c false if the + * Value offset exceeds the document size. + */ + bool pushError(const Value& value, const std::string& message); + + /** \brief Add a semantic error message with extra context. + * \param value JSON Value location associated with the error + * \param message The error message. + * \param extra Additional JSON Value location to contextualize the error + * \return \c true if the error was successfully added, \c false if either + * Value offset exceeds the document size. + */ + bool pushError(const Value& value, const std::string& message, const Value& extra); + + /** \brief Return whether there are any errors. + * \return \c true if there are no errors to report \c false if + * errors have occurred. + */ + bool good() const; + +private: + enum TokenType { + tokenEndOfStream = 0, + tokenObjectBegin, + tokenObjectEnd, + tokenArrayBegin, + tokenArrayEnd, + tokenString, + tokenNumber, + tokenTrue, + tokenFalse, + tokenNull, + tokenArraySeparator, + tokenMemberSeparator, + tokenComment, + tokenError + }; + + class Token { + public: + TokenType type_; + Location start_; + Location end_; + }; + + class ErrorInfo { + public: + Token token_; + std::string message_; + Location extra_; + }; + + typedef std::deque Errors; + + bool readToken(Token& token); + void skipSpaces(); + bool match(Location pattern, int patternLength); + bool readComment(); + bool readCStyleComment(); + bool readCppStyleComment(); + bool readString(); + void readNumber(); + bool readValue(); + bool readObject(Token& token); + bool readArray(Token& token); + bool decodeNumber(Token& token); + bool decodeNumber(Token& token, Value& decoded); + bool decodeString(Token& token); + bool decodeString(Token& token, std::string& decoded); + bool decodeDouble(Token& token); + bool decodeDouble(Token& token, Value& decoded); + bool decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool addError(const std::string& message, Token& token, Location extra = 0); + bool recoverFromError(TokenType skipUntilToken); + bool addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken); + void skipUntilSpace(); + Value& currentValue(); + Char getNextChar(); + void + getLocationLineAndColumn(Location location, int& line, int& column) const; + std::string getLocationLineAndColumn(Location location) const; + void addComment(Location begin, Location end, CommentPlacement placement); + void skipCommentTokens(Token& token); + + typedef std::stack Nodes; + Nodes nodes_; + Errors errors_; + std::string document_; + Location begin_; + Location end_; + Location current_; + Location lastValueEnd_; + Value* lastValue_; + std::string commentsBefore_; + Features features_; + bool collectComments_; +}; // Reader + +/** Interface for reading JSON from a char array. + */ +class JSON_API CharReader { +public: + virtual ~CharReader() {} + /** \brief Read a Value from a JSON + document. + * The document must be a UTF-8 encoded string containing the document to read. + * + * \param beginDoc Pointer on the beginning of the UTF-8 encoded string of the + document to read. + * \param endDoc Pointer on the end of the UTF-8 encoded string of the + document to read. + * Must be >= beginDoc. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param errs [out] Formatted error messages (if not NULL) + * a user friendly string that lists errors in the parsed + * document. + * \return \c true if the document was successfully parsed, \c false if an + error occurred. + */ + virtual bool parse( + char const* beginDoc, char const* endDoc, + Value* root, std::string* errs) = 0; + + class JSON_API Factory { + public: + virtual ~Factory() {} + /** \brief Allocate a CharReader via operator new(). + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + virtual CharReader* newCharReader() const = 0; + }; // Factory +}; // CharReader + +/** \brief Build a CharReader implementation. + +Usage: +\code + using namespace Json; + CharReaderBuilder builder; + builder["collectComments"] = false; + Value value; + std::string errs; + bool ok = parseFromStream(builder, std::cin, &value, &errs); +\endcode +*/ +class JSON_API CharReaderBuilder : public CharReader::Factory { +public: + // Note: We use a Json::Value so that we can add data-members to this class + // without a major version bump. + /** Configuration of this builder. + These are case-sensitive. + Available settings (case-sensitive): + - `"collectComments": false or true` + - true to collect comment and allow writing them + back during serialization, false to discard comments. + This parameter is ignored if allowComments is false. + - `"allowComments": false or true` + - true if comments are allowed. + - `"strictRoot": false or true` + - true if root must be either an array or an object value + - `"allowDroppedNullPlaceholders": false or true` + - true if dropped null placeholders are allowed. (See StreamWriterBuilder.) + - `"allowNumericKeys": false or true` + - true if numeric object keys are allowed. + - `"allowSingleQuotes": false or true` + - true if '' are allowed for strings (both keys and values) + - `"stackLimit": integer` + - Exceeding stackLimit (recursive depth of `readValue()`) will + cause an exception. + - This is a security issue (seg-faults caused by deeply nested JSON), + so the default is low. + - `"failIfExtra": false or true` + - If true, `parse()` returns false when extra non-whitespace trails + the JSON value in the input string. + - `"rejectDupKeys": false or true` + - If true, `parse()` returns false when a key is duplicated within an object. + - `"allowSpecialFloats": false or true` + - If true, special float values (NaNs and infinities) are allowed + and their values are lossfree restorable. + + You can examine 'settings_` yourself + to see the defaults. You can also write and read them just like any + JSON Value. + \sa setDefaults() + */ + Json::Value settings_; + + CharReaderBuilder(); + ~CharReaderBuilder() ; + + CharReader* newCharReader() const ; + + /** \return true if 'settings' are legal and consistent; + * otherwise, indicate bad settings via 'invalid'. + */ + bool validate(Json::Value* invalid) const; + + /** A simple way to update a specific setting. + */ + Value& operator[](std::string key); + + /** Called by ctor, but you can use this to reset settings_. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderDefaults + */ + static void setDefaults(Json::Value* settings); + /** Same as old Features::strictMode(). + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderStrictMode + */ + static void strictMode(Json::Value* settings); +}; + +/** Consume entire stream and use its begin/end. + * Someday we might have a real StreamReader, but for now this + * is convenient. + */ +bool JSON_API parseFromStream( + CharReader::Factory const&, + std::istream&, + Value* root, std::string* errs); + +/** \brief Read from 'sin' into 'root'. + + Always keep comments from the input JSON. + + This can be used to read a file into a particular sub-object. + For example: + \code + Json::Value root; + cin >> root["dir"]["file"]; + cout << root; + \endcode + Result: + \verbatim + { + "dir": { + "file": { + // The input stream JSON would be nested here. + } + } + } + \endverbatim + \throw std::exception on parse error. + \see Json::operator<<() +*/ +JSON_API std::istream& operator>>(std::istream&, Value&); + +} // namespace Json + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // CPPTL_JSON_READER_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/reader.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/writer.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_WRITER_H_INCLUDED +#define JSON_WRITER_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "value.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include + +// Disable warning C4251: : needs to have dll-interface to +// be used by... +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +namespace Json { + +class Value; + +/** + +Usage: +\code + using namespace Json; + void writeToStdout(StreamWriter::Factory const& factory, Value const& value) { + std::unique_ptr const writer( + factory.newStreamWriter()); + writer->write(value, &std::cout); + std::cout << std::endl; // add lf and flush + } +\endcode +*/ +class JSON_API StreamWriter { +protected: + std::ostream* sout_; // not owned; will not delete +public: + StreamWriter(); + virtual ~StreamWriter(); + /** Write Value into document as configured in sub-class. + Do not take ownership of sout, but maintain a reference during function. + \pre sout != NULL + \return zero on success (For now, we always return zero, so check the stream instead.) + \throw std::exception possibly, depending on configuration + */ + virtual int write(Value const& root, std::ostream* sout) = 0; + + /** \brief A simple abstract factory. + */ + class JSON_API Factory { + public: + virtual ~Factory(); + /** \brief Allocate a CharReader via operator new(). + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + virtual StreamWriter* newStreamWriter() const = 0; + }; // Factory +}; // StreamWriter + +/** \brief Write into stringstream, then return string, for convenience. + * A StreamWriter will be created from the factory, used, and then deleted. + */ +std::string JSON_API writeString(StreamWriter::Factory const& factory, Value const& root); + + +/** \brief Build a StreamWriter implementation. + +Usage: +\code + using namespace Json; + Value value = ...; + StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = " "; // or whatever you like + std::unique_ptr writer( + builder.newStreamWriter()); + writer->write(value, &std::cout); + std::cout << std::endl; // add lf and flush +\endcode +*/ +class JSON_API StreamWriterBuilder : public StreamWriter::Factory { +public: + // Note: We use a Json::Value so that we can add data-members to this class + // without a major version bump. + /** Configuration of this builder. + Available settings (case-sensitive): + - "commentStyle": "None" or "All" + - "indentation": "" + - "enableYAMLCompatibility": false or true + - slightly change the whitespace around colons + - "dropNullPlaceholders": false or true + - Drop the "null" string from the writer's output for nullValues. + Strictly speaking, this is not valid JSON. But when the output is being + fed to a browser's Javascript, it makes for smaller output and the + browser can handle the output just fine. + - "useSpecialFloats": false or true + - If true, outputs non-finite floating point values in the following way: + NaN values as "NaN", positive infinity as "Infinity", and negative infinity + as "-Infinity". + + You can examine 'settings_` yourself + to see the defaults. You can also write and read them just like any + JSON Value. + \sa setDefaults() + */ + Json::Value settings_; + + StreamWriterBuilder(); + ~StreamWriterBuilder() ; + + /** + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + StreamWriter* newStreamWriter() const ; + + /** \return true if 'settings' are legal and consistent; + * otherwise, indicate bad settings via 'invalid'. + */ + bool validate(Json::Value* invalid) const; + /** A simple way to update a specific setting. + */ + Value& operator[](std::string key); + + /** Called by ctor, but you can use this to reset settings_. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_writer.cpp StreamWriterBuilderDefaults + */ + static void setDefaults(Json::Value* settings); +}; + +/** \brief Abstract class for writers. + * \deprecated Use StreamWriter. (And really, this is an implementation detail.) + */ +class JSON_API Writer { +public: + virtual ~Writer(); + + virtual std::string write(const Value& root) = 0; +}; + +/** \brief Outputs a Value in JSON format + *without formatting (not human friendly). + * + * The JSON document is written in a single line. It is not intended for 'human' + *consumption, + * but may be usefull to support feature such as RPC where bandwith is limited. + * \sa Reader, Value + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API FastWriter : public Writer { + +public: + FastWriter(); + ~FastWriter() {} + + void enableYAMLCompatibility(); + + /** \brief Drop the "null" string from the writer's output for nullValues. + * Strictly speaking, this is not valid JSON. But when the output is being + * fed to a browser's Javascript, it makes for smaller output and the + * browser can handle the output just fine. + */ + void dropNullPlaceholders(); + + void omitEndingLineFeed(); + +public: // overridden from Writer + std::string write(const Value& root) ; + +private: + void writeValue(const Value& value); + + std::string document_; + bool yamlCompatiblityEnabled_; + bool dropNullPlaceholders_; + bool omitEndingLineFeed_; +}; + +/** \brief Writes a Value in JSON format in a + *human friendly way. + * + * The rules for line break and indent are as follow: + * - Object value: + * - if empty then print {} without indent and line break + * - if not empty the print '{', line break & indent, print one value per + *line + * and then unindent and line break and print '}'. + * - Array value: + * - if empty then print [] without indent and line break + * - if the array contains no object value, empty array or some other value + *types, + * and all the values fit on one lines, then print the array on a single + *line. + * - otherwise, it the values do not fit on one line, or the array contains + * object or non empty array, then print one value per line. + * + * If the Value have comments then they are outputed according to their + *#CommentPlacement. + * + * \sa Reader, Value, Value::setComment() + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API StyledWriter : public Writer { +public: + StyledWriter(); + ~StyledWriter() {} + +public: // overridden from Writer + /** \brief Serialize a Value in JSON format. + * \param root Value to serialize. + * \return String containing the JSON document that represents the root value. + */ + std::string write(const Value& root) ; + +private: + void writeValue(const Value& value); + void writeArrayValue(const Value& value); + bool isMultineArray(const Value& value); + void pushValue(const std::string& value); + void writeIndent(); + void writeWithIndent(const std::string& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(const Value& root); + void writeCommentAfterValueOnSameLine(const Value& root); + bool hasCommentForValue(const Value& value); + static std::string normalizeEOL(const std::string& text); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::string document_; + std::string indentString_; + int rightMargin_; + int indentSize_; + bool addChildValues_; +}; + +/** \brief Writes a Value in JSON format in a + human friendly way, + to a stream rather than to a string. + * + * The rules for line break and indent are as follow: + * - Object value: + * - if empty then print {} without indent and line break + * - if not empty the print '{', line break & indent, print one value per + line + * and then unindent and line break and print '}'. + * - Array value: + * - if empty then print [] without indent and line break + * - if the array contains no object value, empty array or some other value + types, + * and all the values fit on one lines, then print the array on a single + line. + * - otherwise, it the values do not fit on one line, or the array contains + * object or non empty array, then print one value per line. + * + * If the Value have comments then they are outputed according to their + #CommentPlacement. + * + * \param indentation Each level will be indented by this amount extra. + * \sa Reader, Value, Value::setComment() + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API StyledStreamWriter { +public: + StyledStreamWriter(std::string indentation = "\t"); + ~StyledStreamWriter() {} + +public: + /** \brief Serialize a Value in JSON format. + * \param out Stream to write to. (Can be ostringstream, e.g.) + * \param root Value to serialize. + * \note There is no point in deriving from Writer, since write() should not + * return a value. + */ + void write(std::ostream& out, const Value& root); + +private: + void writeValue(const Value& value); + void writeArrayValue(const Value& value); + bool isMultineArray(const Value& value); + void pushValue(const std::string& value); + void writeIndent(); + void writeWithIndent(const std::string& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(const Value& root); + void writeCommentAfterValueOnSameLine(const Value& root); + bool hasCommentForValue(const Value& value); + static std::string normalizeEOL(const std::string& text); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::ostream* document_; + std::string indentString_; + int rightMargin_; + std::string indentation_; + bool addChildValues_ : 1; + bool indented_ : 1; +}; + +#if defined(JSON_HAS_INT64) +std::string JSON_API valueToString(Int value); +std::string JSON_API valueToString(UInt value); +#endif // if defined(JSON_HAS_INT64) +std::string JSON_API valueToString(LargestInt value); +std::string JSON_API valueToString(LargestUInt value); +std::string JSON_API valueToString(double value); +std::string JSON_API valueToString(bool value); +std::string JSON_API valueToQuotedString(const char* value); + +/// \brief Output using the StyledStreamWriter. +/// \see Json::operator>>() +JSON_API std::ostream& operator<<(std::ostream&, const Value& root); + +} // namespace Json + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // JSON_WRITER_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/writer.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/assertions.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_ASSERTIONS_H_INCLUDED +#define CPPTL_JSON_ASSERTIONS_H_INCLUDED + +#include +#include + +#if !defined(JSON_IS_AMALGAMATION) +#include "config.h" +#endif // if !defined(JSON_IS_AMALGAMATION) + +/** It should not be possible for a maliciously designed file to + * cause an abort() or seg-fault, so these macros are used only + * for pre-condition violations and internal logic errors. + */ +#if JSON_USE_EXCEPTION + +// @todo <= add detail about condition in exception +# define JSON_ASSERT(condition) \ + {if (!(condition)) {Json::throwLogicError( "assert json failed" );}} + +# define JSON_FAIL_MESSAGE(message) \ + { \ + std::ostringstream oss; oss << message; \ + Json::throwLogicError(oss.str()); \ + abort(); \ + } + +#else // JSON_USE_EXCEPTION + +# define JSON_ASSERT(condition) assert(condition) + +// The call to assert() will show the failure message in debug builds. In +// release builds we abort, for a core-dump or debugger. +# define JSON_FAIL_MESSAGE(message) \ + { \ + std::ostringstream oss; oss << message; \ + assert(false && oss.str().c_str()); \ + abort(); \ + } + + +#endif + +#define JSON_ASSERT_MESSAGE(condition, message) \ + if (!(condition)) { \ + JSON_FAIL_MESSAGE(message); \ + } + +#endif // CPPTL_JSON_ASSERTIONS_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/assertions.h +// ////////////////////////////////////////////////////////////////////// + + + + + +#endif //ifndef JSON_AMALGATED_H_INCLUDED diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp new file mode 100755 index 000000000..985589c82 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp @@ -0,0 +1,5192 @@ +/// Json-cpp amalgated source (http://jsoncpp.sourceforge.net/). +/// It is intended to be used with #include "json/json.h" + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + +/* +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... + +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. + +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is +released under the terms of the MIT License (see below). + +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. + +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: + + http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +======================================================================== +Copyright (c) 2007-2010 Baptiste Lepilleur + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +======================================================================== +(END LICENSE TEXT) + +The MIT license is compatible with both the GPL and commercial +software, affording one all of the rights of Public Domain with the +minor nuisance of being required to keep the above copyright notice +and license text in the source code. Note also that by accepting the +Public Domain "license" you can re-license your copy using whatever +license you like. + +*/ + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + + + + + + +#include "json/json.h" + +#ifndef JSON_IS_AMALGAMATION +#error "Compile with -I PATH_TO_JSON_DIRECTORY" +#endif + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_tool.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef LIB_JSONCPP_JSON_TOOL_H_INCLUDED +#define LIB_JSONCPP_JSON_TOOL_H_INCLUDED + +/* This header provides common string manipulation support, such as UTF-8, + * portable conversion from/to string... + * + * It is an internal header that must not be exposed. + */ + +namespace Json { + +/// Converts a unicode code-point to UTF-8. +static inline std::string codePointToUTF8(unsigned int cp) { + std::string result; + + // based on description from http://en.wikipedia.org/wiki/UTF-8 + + if (cp <= 0x7f) { + result.resize(1); + result[0] = static_cast(cp); + } else if (cp <= 0x7FF) { + result.resize(2); + result[1] = static_cast(0x80 | (0x3f & cp)); + result[0] = static_cast(0xC0 | (0x1f & (cp >> 6))); + } else if (cp <= 0xFFFF) { + result.resize(3); + result[2] = static_cast(0x80 | (0x3f & cp)); + result[1] = static_cast(0x80 | (0x3f & (cp >> 6))); + result[0] = static_cast(0xE0 | (0xf & (cp >> 12))); + } else if (cp <= 0x10FFFF) { + result.resize(4); + result[3] = static_cast(0x80 | (0x3f & cp)); + result[2] = static_cast(0x80 | (0x3f & (cp >> 6))); + result[1] = static_cast(0x80 | (0x3f & (cp >> 12))); + result[0] = static_cast(0xF0 | (0x7 & (cp >> 18))); + } + + return result; +} + +/// Returns true if ch is a control character (in range [1,31]). +static inline bool isControlCharacter(char ch) { return ch > 0 && ch <= 0x1F; } + +enum { + /// Constant that specify the size of the buffer that must be passed to + /// uintToString. + uintToStringBufferSize = 3 * sizeof(LargestUInt) + 1 +}; + +// Defines a char buffer for use with uintToString(). +typedef char UIntToStringBuffer[uintToStringBufferSize]; + +/** Converts an unsigned integer to string. + * @param value Unsigned interger to convert to string + * @param current Input/Output string buffer. + * Must have at least uintToStringBufferSize chars free. + */ +static inline void uintToString(LargestUInt value, char*& current) { + *--current = 0; + do { + *--current = static_cast(value % 10U + static_cast('0')); + value /= 10; + } while (value != 0); +} + +/** Change ',' to '.' everywhere in buffer. + * + * We had a sophisticated way, but it did not work in WinCE. + * @see https://github.com/open-source-parsers/jsoncpp/pull/9 + */ +static inline void fixNumericLocale(char* begin, char* end) { + while (begin < end) { + if (*begin == ',') { + *begin = '.'; + } + ++begin; + } +} + +} // namespace Json { + +#endif // LIB_JSONCPP_JSON_TOOL_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_tool.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_reader.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include "json_tool.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above +#define snprintf sprintf_s +#elif _MSC_VER >= 1900 // VC++ 14.0 and above +#define snprintf std::snprintf +#else +#define snprintf _snprintf +#endif +#elif defined(__ANDROID__) || defined(__QNXNTO__) +#define snprintf snprintf +#elif __cplusplus >= 201103L +#define snprintf std::snprintf +#endif + +#if defined(__QNXNTO__) +#define sscanf std::sscanf +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 // VC++ 8.0 +// Disable warning about strdup being deprecated. +#pragma warning(disable : 4996) +#endif + +static int const stackLimit_g = 1000; +static int stackDepth_g = 0; // see readValue() + +namespace Json { + +#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) +typedef std::unique_ptr CharReaderPtr; +#else +typedef std::auto_ptr CharReaderPtr; +#endif + +// Implementation of class Features +// //////////////////////////////// + +Features::Features() + : allowComments_(true), strictRoot_(false), + allowDroppedNullPlaceholders_(false), allowNumericKeys_(false) {} + +Features Features::all() { return Features(); } + +Features Features::strictMode() { + Features features; + features.allowComments_ = false; + features.strictRoot_ = true; + features.allowDroppedNullPlaceholders_ = false; + features.allowNumericKeys_ = false; + return features; +} + +// Implementation of class Reader +// //////////////////////////////// + +static bool containsNewLine(Reader::Location begin, Reader::Location end) { + for (; begin < end; ++begin) + if (*begin == '\n' || *begin == '\r') + return true; + return false; +} + +// Class Reader +// ////////////////////////////////////////////////////////////////// + +Reader::Reader() + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), features_(Features::all()), + collectComments_() {} + +Reader::Reader(const Features& features) + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), features_(features), collectComments_() { +} + +bool +Reader::parse(const std::string& document, Value& root, bool collectComments) { + document_ = document; + const char* begin = document_.c_str(); + const char* end = begin + document_.length(); + return parse(begin, end, root, collectComments); +} + +bool Reader::parse(std::istream& sin, Value& root, bool collectComments) { + // std::istream_iterator begin(sin); + // std::istream_iterator end; + // Those would allow streamed input from a file, if parse() were a + // template function. + + // Since std::string is reference-counted, this at least does not + // create an extra copy. + std::string doc; + std::getline(sin, doc, (char)EOF); + return parse(doc, root, collectComments); +} + +bool Reader::parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments) { + if (!features_.allowComments_) { + collectComments = false; + } + + begin_ = beginDoc; + end_ = endDoc; + collectComments_ = collectComments; + current_ = begin_; + lastValueEnd_ = 0; + lastValue_ = 0; + commentsBefore_ = ""; + errors_.clear(); + while (!nodes_.empty()) + nodes_.pop(); + nodes_.push(&root); + + stackDepth_g = 0; // Yes, this is bad coding, but options are limited. + bool successful = readValue(); + Token token; + skipCommentTokens(token); + if (collectComments_ && !commentsBefore_.empty()) + root.setComment(commentsBefore_, commentAfter); + if (features_.strictRoot_) { + if (!root.isArray() && !root.isObject()) { + // Set error location to start of doc, ideally should be first token found + // in doc + token.type_ = tokenError; + token.start_ = beginDoc; + token.end_ = endDoc; + addError( + "A valid JSON document must be either an array or an object value.", + token); + return false; + } + } + return successful; +} + +bool Reader::readValue() { + // This is a non-reentrant way to support a stackLimit. Terrible! + // But this deprecated class has a security problem: Bad input can + // cause a seg-fault. This seems like a fair, binary-compatible way + // to prevent the problem. + if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue()."); + ++stackDepth_g; + + Token token; + skipCommentTokens(token); + bool successful = true; + + if (collectComments_ && !commentsBefore_.empty()) { + currentValue().setComment(commentsBefore_, commentBefore); + commentsBefore_ = ""; + } + + switch (token.type_) { + case tokenObjectBegin: + successful = readObject(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenArrayBegin: + successful = readArray(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenNumber: + successful = decodeNumber(token); + break; + case tokenString: + successful = decodeString(token); + break; + case tokenTrue: + { + Value v(true); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenFalse: + { + Value v(false); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNull: + { + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenArraySeparator: + case tokenObjectEnd: + case tokenArrayEnd: + if (features_.allowDroppedNullPlaceholders_) { + // "Un-read" the current token and mark the current value as a null + // token. + current_--; + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(current_ - begin_ - 1); + currentValue().setOffsetLimit(current_ - begin_); + break; + } // Else, fall through... + default: + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return addError("Syntax error: value, object or array expected.", token); + } + + if (collectComments_) { + lastValueEnd_ = current_; + lastValue_ = ¤tValue(); + } + + --stackDepth_g; + return successful; +} + +void Reader::skipCommentTokens(Token& token) { + if (features_.allowComments_) { + do { + readToken(token); + } while (token.type_ == tokenComment); + } else { + readToken(token); + } +} + +bool Reader::readToken(Token& token) { + skipSpaces(); + token.start_ = current_; + Char c = getNextChar(); + bool ok = true; + switch (c) { + case '{': + token.type_ = tokenObjectBegin; + break; + case '}': + token.type_ = tokenObjectEnd; + break; + case '[': + token.type_ = tokenArrayBegin; + break; + case ']': + token.type_ = tokenArrayEnd; + break; + case '"': + token.type_ = tokenString; + ok = readString(); + break; + case '/': + token.type_ = tokenComment; + ok = readComment(); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + token.type_ = tokenNumber; + readNumber(); + break; + case 't': + token.type_ = tokenTrue; + ok = match("rue", 3); + break; + case 'f': + token.type_ = tokenFalse; + ok = match("alse", 4); + break; + case 'n': + token.type_ = tokenNull; + ok = match("ull", 3); + break; + case ',': + token.type_ = tokenArraySeparator; + break; + case ':': + token.type_ = tokenMemberSeparator; + break; + case 0: + token.type_ = tokenEndOfStream; + break; + default: + ok = false; + break; + } + if (!ok) + token.type_ = tokenError; + token.end_ = current_; + return true; +} + +void Reader::skipSpaces() { + while (current_ != end_) { + Char c = *current_; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + ++current_; + else + break; + } +} + +bool Reader::match(Location pattern, int patternLength) { + if (end_ - current_ < patternLength) + return false; + int index = patternLength; + while (index--) + if (current_[index] != pattern[index]) + return false; + current_ += patternLength; + return true; +} + +bool Reader::readComment() { + Location commentBegin = current_ - 1; + Char c = getNextChar(); + bool successful = false; + if (c == '*') + successful = readCStyleComment(); + else if (c == '/') + successful = readCppStyleComment(); + if (!successful) + return false; + + if (collectComments_) { + CommentPlacement placement = commentBefore; + if (lastValueEnd_ && !containsNewLine(lastValueEnd_, commentBegin)) { + if (c != '*' || !containsNewLine(commentBegin, current_)) + placement = commentAfterOnSameLine; + } + + addComment(commentBegin, current_, placement); + } + return true; +} + +static std::string normalizeEOL(Reader::Location begin, Reader::Location end) { + std::string normalized; + normalized.reserve(end - begin); + Reader::Location current = begin; + while (current != end) { + char c = *current++; + if (c == '\r') { + if (current != end && *current == '\n') + // convert dos EOL + ++current; + // convert Mac EOL + normalized += '\n'; + } else { + normalized += c; + } + } + return normalized; +} + +void +Reader::addComment(Location begin, Location end, CommentPlacement placement) { + assert(collectComments_); + const std::string& normalized = normalizeEOL(begin, end); + if (placement == commentAfterOnSameLine) { + assert(lastValue_ != 0); + lastValue_->setComment(normalized, placement); + } else { + commentsBefore_ += normalized; + } +} + +bool Reader::readCStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '*' && *current_ == '/') + break; + } + return getNextChar() == '/'; +} + +bool Reader::readCppStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '\n') + break; + if (c == '\r') { + // Consume DOS EOL. It will be normalized in addComment. + if (current_ != end_ && *current_ == '\n') + getNextChar(); + // Break on Moc OS 9 EOL. + break; + } + } + return true; +} + +void Reader::readNumber() { + const char *p = current_; + char c = '0'; // stopgap for already consumed character + // integral part + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + // fractional part + if (c == '.') { + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + // exponential part + if (c == 'e' || c == 'E') { + c = (current_ = p) < end_ ? *p++ : 0; + if (c == '+' || c == '-') + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } +} + +bool Reader::readString() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '"') + break; + } + return c == '"'; +} + +bool Reader::readObject(Token& tokenStart) { + Token tokenName; + std::string name; + Value init(objectValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + while (readToken(tokenName)) { + bool initialTokenOk = true; + while (tokenName.type_ == tokenComment && initialTokenOk) + initialTokenOk = readToken(tokenName); + if (!initialTokenOk) + break; + if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object + return true; + name = ""; + if (tokenName.type_ == tokenString) { + if (!decodeString(tokenName, name)) + return recoverFromError(tokenObjectEnd); + } else if (tokenName.type_ == tokenNumber && features_.allowNumericKeys_) { + Value numberName; + if (!decodeNumber(tokenName, numberName)) + return recoverFromError(tokenObjectEnd); + name = numberName.asString(); + } else { + break; + } + + Token colon; + if (!readToken(colon) || colon.type_ != tokenMemberSeparator) { + return addErrorAndRecover( + "Missing ':' after object member name", colon, tokenObjectEnd); + } + Value& value = currentValue()[name]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenObjectEnd); + + Token comma; + if (!readToken(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && + comma.type_ != tokenComment)) { + return addErrorAndRecover( + "Missing ',' or '}' in object declaration", comma, tokenObjectEnd); + } + bool finalizeTokenOk = true; + while (comma.type_ == tokenComment && finalizeTokenOk) + finalizeTokenOk = readToken(comma); + if (comma.type_ == tokenObjectEnd) + return true; + } + return addErrorAndRecover( + "Missing '}' or object member name", tokenName, tokenObjectEnd); +} + +bool Reader::readArray(Token& tokenStart) { + Value init(arrayValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + skipSpaces(); + if (*current_ == ']') // empty array + { + Token endArray; + readToken(endArray); + return true; + } + int index = 0; + for (;;) { + Value& value = currentValue()[index++]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenArrayEnd); + + Token token; + // Accept Comment after last item in the array. + ok = readToken(token); + while (token.type_ == tokenComment && ok) { + ok = readToken(token); + } + bool badTokenType = + (token.type_ != tokenArraySeparator && token.type_ != tokenArrayEnd); + if (!ok || badTokenType) { + return addErrorAndRecover( + "Missing ',' or ']' in array declaration", token, tokenArrayEnd); + } + if (token.type_ == tokenArrayEnd) + break; + } + return true; +} + +bool Reader::decodeNumber(Token& token) { + Value decoded; + if (!decodeNumber(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeNumber(Token& token, Value& decoded) { + // Attempts to parse the number as an integer. If the number is + // larger than the maximum supported value of an integer then + // we decode the number as a double. + Location current = token.start_; + bool isNegative = *current == '-'; + if (isNegative) + ++current; + // TODO: Help the compiler do the div and mod at compile time or get rid of them. + Value::LargestUInt maxIntegerValue = + isNegative ? Value::LargestUInt(Value::maxLargestInt) + 1 + : Value::maxLargestUInt; + Value::LargestUInt threshold = maxIntegerValue / 10; + Value::LargestUInt value = 0; + while (current < token.end_) { + Char c = *current++; + if (c < '0' || c > '9') + return decodeDouble(token, decoded); + Value::UInt digit(c - '0'); + if (value >= threshold) { + // We've hit or exceeded the max value divided by 10 (rounded down). If + // a) we've only just touched the limit, b) this is the last digit, and + // c) it's small enough to fit in that rounding delta, we're okay. + // Otherwise treat this number as a double to avoid overflow. + if (value > threshold || current != token.end_ || + digit > maxIntegerValue % 10) { + return decodeDouble(token, decoded); + } + } + value = value * 10 + digit; + } + if (isNegative && value == maxIntegerValue) + decoded = Value::minLargestInt; + else if (isNegative) + decoded = -Value::LargestInt(value); + else if (value <= Value::LargestUInt(Value::maxInt)) + decoded = Value::LargestInt(value); + else + decoded = value; + return true; +} + +bool Reader::decodeDouble(Token& token) { + Value decoded; + if (!decodeDouble(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeDouble(Token& token, Value& decoded) { + double value = 0; + std::string buffer(token.start_, token.end_); + std::istringstream is(buffer); + if (!(is >> value)) + return addError("'" + std::string(token.start_, token.end_) + + "' is not a number.", + token); + decoded = value; + return true; +} + +bool Reader::decodeString(Token& token) { + std::string decoded_string; + if (!decodeString(token, decoded_string)) + return false; + Value decoded(decoded_string); + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeString(Token& token, std::string& decoded) { + decoded.reserve(token.end_ - token.start_ - 2); + Location current = token.start_ + 1; // skip '"' + Location end = token.end_ - 1; // do not include '"' + while (current != end) { + Char c = *current++; + if (c == '"') + break; + else if (c == '\\') { + if (current == end) + return addError("Empty escape sequence in string", token, current); + Char escape = *current++; + switch (escape) { + case '"': + decoded += '"'; + break; + case '/': + decoded += '/'; + break; + case '\\': + decoded += '\\'; + break; + case 'b': + decoded += '\b'; + break; + case 'f': + decoded += '\f'; + break; + case 'n': + decoded += '\n'; + break; + case 'r': + decoded += '\r'; + break; + case 't': + decoded += '\t'; + break; + case 'u': { + unsigned int unicode; + if (!decodeUnicodeCodePoint(token, current, end, unicode)) + return false; + decoded += codePointToUTF8(unicode); + } break; + default: + return addError("Bad escape sequence in string", token, current); + } + } else { + decoded += c; + } + } + return true; +} + +bool Reader::decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + + if (!decodeUnicodeEscapeSequence(token, current, end, unicode)) + return false; + if (unicode >= 0xD800 && unicode <= 0xDBFF) { + // surrogate pairs + if (end - current < 6) + return addError( + "additional six characters expected to parse unicode surrogate pair.", + token, + current); + unsigned int surrogatePair; + if (*(current++) == '\\' && *(current++) == 'u') { + if (decodeUnicodeEscapeSequence(token, current, end, surrogatePair)) { + unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF); + } else + return false; + } else + return addError("expecting another \\u token to begin the second half of " + "a unicode surrogate pair", + token, + current); + } + return true; +} + +bool Reader::decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + if (end - current < 4) + return addError( + "Bad unicode escape sequence in string: four digits expected.", + token, + current); + unicode = 0; + for (int index = 0; index < 4; ++index) { + Char c = *current++; + unicode *= 16; + if (c >= '0' && c <= '9') + unicode += c - '0'; + else if (c >= 'a' && c <= 'f') + unicode += c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + unicode += c - 'A' + 10; + else + return addError( + "Bad unicode escape sequence in string: hexadecimal digit expected.", + token, + current); + } + return true; +} + +bool +Reader::addError(const std::string& message, Token& token, Location extra) { + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = extra; + errors_.push_back(info); + return false; +} + +bool Reader::recoverFromError(TokenType skipUntilToken) { + int errorCount = int(errors_.size()); + Token skip; + for (;;) { + if (!readToken(skip)) + errors_.resize(errorCount); // discard errors caused by recovery + if (skip.type_ == skipUntilToken || skip.type_ == tokenEndOfStream) + break; + } + errors_.resize(errorCount); + return false; +} + +bool Reader::addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken) { + addError(message, token); + return recoverFromError(skipUntilToken); +} + +Value& Reader::currentValue() { return *(nodes_.top()); } + +Reader::Char Reader::getNextChar() { + if (current_ == end_) + return 0; + return *current_++; +} + +void Reader::getLocationLineAndColumn(Location location, + int& line, + int& column) const { + Location current = begin_; + Location lastLineStart = current; + line = 0; + while (current < location && current != end_) { + Char c = *current++; + if (c == '\r') { + if (*current == '\n') + ++current; + lastLineStart = current; + ++line; + } else if (c == '\n') { + lastLineStart = current; + ++line; + } + } + // column & line start at 1 + column = int(location - lastLineStart) + 1; + ++line; +} + +std::string Reader::getLocationLineAndColumn(Location location) const { + int line, column; + getLocationLineAndColumn(location, line, column); + char buffer[18 + 16 + 16 + 1]; + snprintf(buffer, sizeof(buffer), "Line %d, Column %d", line, column); + return buffer; +} + +// Deprecated. Preserved for backward compatibility +std::string Reader::getFormatedErrorMessages() const { + return getFormattedErrorMessages(); +} + +std::string Reader::getFormattedErrorMessages() const { + std::string formattedMessage; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + formattedMessage += + "* " + getLocationLineAndColumn(error.token_.start_) + "\n"; + formattedMessage += " " + error.message_ + "\n"; + if (error.extra_) + formattedMessage += + "See " + getLocationLineAndColumn(error.extra_) + " for detail.\n"; + } + return formattedMessage; +} + +std::vector Reader::getStructuredErrors() const { + std::vector allErrors; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + Reader::StructuredError structured; + structured.offset_start = error.token_.start_ - begin_; + structured.offset_limit = error.token_.end_ - begin_; + structured.message = error.message_; + allErrors.push_back(structured); + } + return allErrors; +} + +bool Reader::pushError(const Value& value, const std::string& message) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = end_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = 0; + errors_.push_back(info); + return true; +} + +bool Reader::pushError(const Value& value, const std::string& message, const Value& extra) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length + || extra.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = begin_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = begin_ + extra.getOffsetStart(); + errors_.push_back(info); + return true; +} + +bool Reader::good() const { + return !errors_.size(); +} + +// exact copy of Features +class OurFeatures { +public: + static OurFeatures all(); + bool allowComments_; + bool strictRoot_; + bool allowDroppedNullPlaceholders_; + bool allowNumericKeys_; + bool allowSingleQuotes_; + bool failIfExtra_; + bool rejectDupKeys_; + bool allowSpecialFloats_; + int stackLimit_; +}; // OurFeatures + +// exact copy of Implementation of class Features +// //////////////////////////////// + +OurFeatures OurFeatures::all() { return OurFeatures(); } + +// Implementation of class Reader +// //////////////////////////////// + +// exact copy of Reader, renamed to OurReader +class OurReader { +public: + typedef char Char; + typedef const Char* Location; + struct StructuredError { + size_t offset_start; + size_t offset_limit; + std::string message; + }; + + OurReader(OurFeatures const& features); + bool parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments = true); + std::string getFormattedErrorMessages() const; + std::vector getStructuredErrors() const; + bool pushError(const Value& value, const std::string& message); + bool pushError(const Value& value, const std::string& message, const Value& extra); + bool good() const; + +private: + OurReader(OurReader const&); // no impl + void operator=(OurReader const&); // no impl + + enum TokenType { + tokenEndOfStream = 0, + tokenObjectBegin, + tokenObjectEnd, + tokenArrayBegin, + tokenArrayEnd, + tokenString, + tokenNumber, + tokenTrue, + tokenFalse, + tokenNull, + tokenNaN, + tokenPosInf, + tokenNegInf, + tokenArraySeparator, + tokenMemberSeparator, + tokenComment, + tokenError + }; + + class Token { + public: + TokenType type_; + Location start_; + Location end_; + }; + + class ErrorInfo { + public: + Token token_; + std::string message_; + Location extra_; + }; + + typedef std::deque Errors; + + bool readToken(Token& token); + void skipSpaces(); + bool match(Location pattern, int patternLength); + bool readComment(); + bool readCStyleComment(); + bool readCppStyleComment(); + bool readString(); + bool readStringSingleQuote(); + bool readNumber(bool checkInf); + bool readValue(); + bool readObject(Token& token); + bool readArray(Token& token); + bool decodeNumber(Token& token); + bool decodeNumber(Token& token, Value& decoded); + bool decodeString(Token& token); + bool decodeString(Token& token, std::string& decoded); + bool decodeDouble(Token& token); + bool decodeDouble(Token& token, Value& decoded); + bool decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool addError(const std::string& message, Token& token, Location extra = 0); + bool recoverFromError(TokenType skipUntilToken); + bool addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken); + void skipUntilSpace(); + Value& currentValue(); + Char getNextChar(); + void + getLocationLineAndColumn(Location location, int& line, int& column) const; + std::string getLocationLineAndColumn(Location location) const; + void addComment(Location begin, Location end, CommentPlacement placement); + void skipCommentTokens(Token& token); + + typedef std::stack Nodes; + Nodes nodes_; + Errors errors_; + std::string document_; + Location begin_; + Location end_; + Location current_; + Location lastValueEnd_; + Value* lastValue_; + std::string commentsBefore_; + int stackDepth_; + + OurFeatures const features_; + bool collectComments_; +}; // OurReader + +// complete copy of Read impl, for OurReader + +OurReader::OurReader(OurFeatures const& features) + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), + stackDepth_(0), + features_(features), collectComments_() { +} + +bool OurReader::parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments) { + if (!features_.allowComments_) { + collectComments = false; + } + + begin_ = beginDoc; + end_ = endDoc; + collectComments_ = collectComments; + current_ = begin_; + lastValueEnd_ = 0; + lastValue_ = 0; + commentsBefore_ = ""; + errors_.clear(); + while (!nodes_.empty()) + nodes_.pop(); + nodes_.push(&root); + + stackDepth_ = 0; + bool successful = readValue(); + Token token; + skipCommentTokens(token); + if (features_.failIfExtra_) { + if (token.type_ != tokenError && token.type_ != tokenEndOfStream) { + addError("Extra non-whitespace after JSON value.", token); + return false; + } + } + if (collectComments_ && !commentsBefore_.empty()) + root.setComment(commentsBefore_, commentAfter); + if (features_.strictRoot_) { + if (!root.isArray() && !root.isObject()) { + // Set error location to start of doc, ideally should be first token found + // in doc + token.type_ = tokenError; + token.start_ = beginDoc; + token.end_ = endDoc; + addError( + "A valid JSON document must be either an array or an object value.", + token); + return false; + } + } + return successful; +} + +bool OurReader::readValue() { + if (stackDepth_ >= features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue()."); + ++stackDepth_; + Token token; + skipCommentTokens(token); + bool successful = true; + + if (collectComments_ && !commentsBefore_.empty()) { + currentValue().setComment(commentsBefore_, commentBefore); + commentsBefore_ = ""; + } + + switch (token.type_) { + case tokenObjectBegin: + successful = readObject(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenArrayBegin: + successful = readArray(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenNumber: + successful = decodeNumber(token); + break; + case tokenString: + successful = decodeString(token); + break; + case tokenTrue: + { + Value v(true); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenFalse: + { + Value v(false); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNull: + { + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNaN: + { + Value v(std::numeric_limits::quiet_NaN()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenPosInf: + { + Value v(std::numeric_limits::infinity()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNegInf: + { + Value v(-std::numeric_limits::infinity()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenArraySeparator: + case tokenObjectEnd: + case tokenArrayEnd: + if (features_.allowDroppedNullPlaceholders_) { + // "Un-read" the current token and mark the current value as a null + // token. + current_--; + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(current_ - begin_ - 1); + currentValue().setOffsetLimit(current_ - begin_); + break; + } // else, fall through ... + default: + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return addError("Syntax error: value, object or array expected.", token); + } + + if (collectComments_) { + lastValueEnd_ = current_; + lastValue_ = ¤tValue(); + } + + --stackDepth_; + return successful; +} + +void OurReader::skipCommentTokens(Token& token) { + if (features_.allowComments_) { + do { + readToken(token); + } while (token.type_ == tokenComment); + } else { + readToken(token); + } +} + +bool OurReader::readToken(Token& token) { + skipSpaces(); + token.start_ = current_; + Char c = getNextChar(); + bool ok = true; + switch (c) { + case '{': + token.type_ = tokenObjectBegin; + break; + case '}': + token.type_ = tokenObjectEnd; + break; + case '[': + token.type_ = tokenArrayBegin; + break; + case ']': + token.type_ = tokenArrayEnd; + break; + case '"': + token.type_ = tokenString; + ok = readString(); + break; + case '\'': + if (features_.allowSingleQuotes_) { + token.type_ = tokenString; + ok = readStringSingleQuote(); + break; + } // else continue + case '/': + token.type_ = tokenComment; + ok = readComment(); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + token.type_ = tokenNumber; + readNumber(false); + break; + case '-': + if (readNumber(true)) { + token.type_ = tokenNumber; + } else { + token.type_ = tokenNegInf; + ok = features_.allowSpecialFloats_ && match("nfinity", 7); + } + break; + case 't': + token.type_ = tokenTrue; + ok = match("rue", 3); + break; + case 'f': + token.type_ = tokenFalse; + ok = match("alse", 4); + break; + case 'n': + token.type_ = tokenNull; + ok = match("ull", 3); + break; + case 'N': + if (features_.allowSpecialFloats_) { + token.type_ = tokenNaN; + ok = match("aN", 2); + } else { + ok = false; + } + break; + case 'I': + if (features_.allowSpecialFloats_) { + token.type_ = tokenPosInf; + ok = match("nfinity", 7); + } else { + ok = false; + } + break; + case ',': + token.type_ = tokenArraySeparator; + break; + case ':': + token.type_ = tokenMemberSeparator; + break; + case 0: + token.type_ = tokenEndOfStream; + break; + default: + ok = false; + break; + } + if (!ok) + token.type_ = tokenError; + token.end_ = current_; + return true; +} + +void OurReader::skipSpaces() { + while (current_ != end_) { + Char c = *current_; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + ++current_; + else + break; + } +} + +bool OurReader::match(Location pattern, int patternLength) { + if (end_ - current_ < patternLength) + return false; + int index = patternLength; + while (index--) + if (current_[index] != pattern[index]) + return false; + current_ += patternLength; + return true; +} + +bool OurReader::readComment() { + Location commentBegin = current_ - 1; + Char c = getNextChar(); + bool successful = false; + if (c == '*') + successful = readCStyleComment(); + else if (c == '/') + successful = readCppStyleComment(); + if (!successful) + return false; + + if (collectComments_) { + CommentPlacement placement = commentBefore; + if (lastValueEnd_ && !containsNewLine(lastValueEnd_, commentBegin)) { + if (c != '*' || !containsNewLine(commentBegin, current_)) + placement = commentAfterOnSameLine; + } + + addComment(commentBegin, current_, placement); + } + return true; +} + +void +OurReader::addComment(Location begin, Location end, CommentPlacement placement) { + assert(collectComments_); + const std::string& normalized = normalizeEOL(begin, end); + if (placement == commentAfterOnSameLine) { + assert(lastValue_ != 0); + lastValue_->setComment(normalized, placement); + } else { + commentsBefore_ += normalized; + } +} + +bool OurReader::readCStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '*' && *current_ == '/') + break; + } + return getNextChar() == '/'; +} + +bool OurReader::readCppStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '\n') + break; + if (c == '\r') { + // Consume DOS EOL. It will be normalized in addComment. + if (current_ != end_ && *current_ == '\n') + getNextChar(); + // Break on Moc OS 9 EOL. + break; + } + } + return true; +} + +bool OurReader::readNumber(bool checkInf) { + const char *p = current_; + if (checkInf && p != end_ && *p == 'I') { + current_ = ++p; + return false; + } + char c = '0'; // stopgap for already consumed character + // integral part + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + // fractional part + if (c == '.') { + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + // exponential part + if (c == 'e' || c == 'E') { + c = (current_ = p) < end_ ? *p++ : 0; + if (c == '+' || c == '-') + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + return true; +} +bool OurReader::readString() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '"') + break; + } + return c == '"'; +} + + +bool OurReader::readStringSingleQuote() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '\'') + break; + } + return c == '\''; +} + +bool OurReader::readObject(Token& tokenStart) { + Token tokenName; + std::string name; + Value init(objectValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + while (readToken(tokenName)) { + bool initialTokenOk = true; + while (tokenName.type_ == tokenComment && initialTokenOk) + initialTokenOk = readToken(tokenName); + if (!initialTokenOk) + break; + if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object + return true; + name = ""; + if (tokenName.type_ == tokenString) { + if (!decodeString(tokenName, name)) + return recoverFromError(tokenObjectEnd); + } else if (tokenName.type_ == tokenNumber && features_.allowNumericKeys_) { + Value numberName; + if (!decodeNumber(tokenName, numberName)) + return recoverFromError(tokenObjectEnd); + name = numberName.asString(); + } else { + break; + } + + Token colon; + if (!readToken(colon) || colon.type_ != tokenMemberSeparator) { + return addErrorAndRecover( + "Missing ':' after object member name", colon, tokenObjectEnd); + } + if (name.length() >= (1U<<30)) throwRuntimeError("keylength >= 2^30"); + if (features_.rejectDupKeys_ && currentValue().isMember(name)) { + std::string msg = "Duplicate key: '" + name + "'"; + return addErrorAndRecover( + msg, tokenName, tokenObjectEnd); + } + Value& value = currentValue()[name]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenObjectEnd); + + Token comma; + if (!readToken(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && + comma.type_ != tokenComment)) { + return addErrorAndRecover( + "Missing ',' or '}' in object declaration", comma, tokenObjectEnd); + } + bool finalizeTokenOk = true; + while (comma.type_ == tokenComment && finalizeTokenOk) + finalizeTokenOk = readToken(comma); + if (comma.type_ == tokenObjectEnd) + return true; + } + return addErrorAndRecover( + "Missing '}' or object member name", tokenName, tokenObjectEnd); +} + +bool OurReader::readArray(Token& tokenStart) { + Value init(arrayValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + skipSpaces(); + if (*current_ == ']') // empty array + { + Token endArray; + readToken(endArray); + return true; + } + int index = 0; + for (;;) { + Value& value = currentValue()[index++]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenArrayEnd); + + Token token; + // Accept Comment after last item in the array. + ok = readToken(token); + while (token.type_ == tokenComment && ok) { + ok = readToken(token); + } + bool badTokenType = + (token.type_ != tokenArraySeparator && token.type_ != tokenArrayEnd); + if (!ok || badTokenType) { + return addErrorAndRecover( + "Missing ',' or ']' in array declaration", token, tokenArrayEnd); + } + if (token.type_ == tokenArrayEnd) + break; + } + return true; +} + +bool OurReader::decodeNumber(Token& token) { + Value decoded; + if (!decodeNumber(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeNumber(Token& token, Value& decoded) { + // Attempts to parse the number as an integer. If the number is + // larger than the maximum supported value of an integer then + // we decode the number as a double. + Location current = token.start_; + bool isNegative = *current == '-'; + if (isNegative) + ++current; + // TODO: Help the compiler do the div and mod at compile time or get rid of them. + Value::LargestUInt maxIntegerValue = + isNegative ? Value::LargestUInt(-Value::minLargestInt) + : Value::maxLargestUInt; + Value::LargestUInt threshold = maxIntegerValue / 10; + Value::LargestUInt value = 0; + while (current < token.end_) { + Char c = *current++; + if (c < '0' || c > '9') + return decodeDouble(token, decoded); + Value::UInt digit(c - '0'); + if (value >= threshold) { + // We've hit or exceeded the max value divided by 10 (rounded down). If + // a) we've only just touched the limit, b) this is the last digit, and + // c) it's small enough to fit in that rounding delta, we're okay. + // Otherwise treat this number as a double to avoid overflow. + if (value > threshold || current != token.end_ || + digit > maxIntegerValue % 10) { + return decodeDouble(token, decoded); + } + } + value = value * 10 + digit; + } + if (isNegative) + decoded = -Value::LargestInt(value); + else if (value <= Value::LargestUInt(Value::maxInt)) + decoded = Value::LargestInt(value); + else + decoded = value; + return true; +} + +bool OurReader::decodeDouble(Token& token) { + Value decoded; + if (!decodeDouble(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeDouble(Token& token, Value& decoded) { + double value = 0; + const int bufferSize = 32; + int count; + int length = int(token.end_ - token.start_); + + // Sanity check to avoid buffer overflow exploits. + if (length < 0) { + return addError("Unable to parse token length", token); + } + + // Avoid using a string constant for the format control string given to + // sscanf, as this can cause hard to debug crashes on OS X. See here for more + // info: + // + // http://developer.apple.com/library/mac/#DOCUMENTATION/DeveloperTools/gcc-4.0.1/gcc/Incompatibilities.html + char format[] = "%lf"; + + if (length <= bufferSize) { + Char buffer[bufferSize + 1]; + memcpy(buffer, token.start_, length); + buffer[length] = 0; + count = sscanf(buffer, format, &value); + } else { + std::string buffer(token.start_, token.end_); + count = sscanf(buffer.c_str(), format, &value); + } + + if (count != 1) + return addError("'" + std::string(token.start_, token.end_) + + "' is not a number.", + token); + decoded = value; + return true; +} + +bool OurReader::decodeString(Token& token) { + std::string decoded_string; + if (!decodeString(token, decoded_string)) + return false; + Value decoded(decoded_string); + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeString(Token& token, std::string& decoded) { + decoded.reserve(token.end_ - token.start_ - 2); + Location current = token.start_ + 1; // skip '"' + Location end = token.end_ - 1; // do not include '"' + while (current != end) { + Char c = *current++; + if (c == '"') + break; + else if (c == '\\') { + if (current == end) + return addError("Empty escape sequence in string", token, current); + Char escape = *current++; + switch (escape) { + case '"': + decoded += '"'; + break; + case '/': + decoded += '/'; + break; + case '\\': + decoded += '\\'; + break; + case 'b': + decoded += '\b'; + break; + case 'f': + decoded += '\f'; + break; + case 'n': + decoded += '\n'; + break; + case 'r': + decoded += '\r'; + break; + case 't': + decoded += '\t'; + break; + case 'u': { + unsigned int unicode; + if (!decodeUnicodeCodePoint(token, current, end, unicode)) + return false; + decoded += codePointToUTF8(unicode); + } break; + default: + return addError("Bad escape sequence in string", token, current); + } + } else { + decoded += c; + } + } + return true; +} + +bool OurReader::decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + + if (!decodeUnicodeEscapeSequence(token, current, end, unicode)) + return false; + if (unicode >= 0xD800 && unicode <= 0xDBFF) { + // surrogate pairs + if (end - current < 6) + return addError( + "additional six characters expected to parse unicode surrogate pair.", + token, + current); + unsigned int surrogatePair; + if (*(current++) == '\\' && *(current++) == 'u') { + if (decodeUnicodeEscapeSequence(token, current, end, surrogatePair)) { + unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF); + } else + return false; + } else + return addError("expecting another \\u token to begin the second half of " + "a unicode surrogate pair", + token, + current); + } + return true; +} + +bool OurReader::decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + if (end - current < 4) + return addError( + "Bad unicode escape sequence in string: four digits expected.", + token, + current); + unicode = 0; + for (int index = 0; index < 4; ++index) { + Char c = *current++; + unicode *= 16; + if (c >= '0' && c <= '9') + unicode += c - '0'; + else if (c >= 'a' && c <= 'f') + unicode += c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + unicode += c - 'A' + 10; + else + return addError( + "Bad unicode escape sequence in string: hexadecimal digit expected.", + token, + current); + } + return true; +} + +bool +OurReader::addError(const std::string& message, Token& token, Location extra) { + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = extra; + errors_.push_back(info); + return false; +} + +bool OurReader::recoverFromError(TokenType skipUntilToken) { + int errorCount = int(errors_.size()); + Token skip; + for (;;) { + if (!readToken(skip)) + errors_.resize(errorCount); // discard errors caused by recovery + if (skip.type_ == skipUntilToken || skip.type_ == tokenEndOfStream) + break; + } + errors_.resize(errorCount); + return false; +} + +bool OurReader::addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken) { + addError(message, token); + return recoverFromError(skipUntilToken); +} + +Value& OurReader::currentValue() { return *(nodes_.top()); } + +OurReader::Char OurReader::getNextChar() { + if (current_ == end_) + return 0; + return *current_++; +} + +void OurReader::getLocationLineAndColumn(Location location, + int& line, + int& column) const { + Location current = begin_; + Location lastLineStart = current; + line = 0; + while (current < location && current != end_) { + Char c = *current++; + if (c == '\r') { + if (*current == '\n') + ++current; + lastLineStart = current; + ++line; + } else if (c == '\n') { + lastLineStart = current; + ++line; + } + } + // column & line start at 1 + column = int(location - lastLineStart) + 1; + ++line; +} + +std::string OurReader::getLocationLineAndColumn(Location location) const { + int line, column; + getLocationLineAndColumn(location, line, column); + char buffer[18 + 16 + 16 + 1]; + snprintf(buffer, sizeof(buffer), "Line %d, Column %d", line, column); + return buffer; +} + +std::string OurReader::getFormattedErrorMessages() const { + std::string formattedMessage; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + formattedMessage += + "* " + getLocationLineAndColumn(error.token_.start_) + "\n"; + formattedMessage += " " + error.message_ + "\n"; + if (error.extra_) + formattedMessage += + "See " + getLocationLineAndColumn(error.extra_) + " for detail.\n"; + } + return formattedMessage; +} + +std::vector OurReader::getStructuredErrors() const { + std::vector allErrors; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + OurReader::StructuredError structured; + structured.offset_start = error.token_.start_ - begin_; + structured.offset_limit = error.token_.end_ - begin_; + structured.message = error.message_; + allErrors.push_back(structured); + } + return allErrors; +} + +bool OurReader::pushError(const Value& value, const std::string& message) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = end_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = 0; + errors_.push_back(info); + return true; +} + +bool OurReader::pushError(const Value& value, const std::string& message, const Value& extra) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length + || extra.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = begin_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = begin_ + extra.getOffsetStart(); + errors_.push_back(info); + return true; +} + +bool OurReader::good() const { + return !errors_.size(); +} + + +class OurCharReader : public CharReader { + bool const collectComments_; + OurReader reader_; +public: + OurCharReader( + bool collectComments, + OurFeatures const& features) + : collectComments_(collectComments) + , reader_(features) + {} + bool parse( + char const* beginDoc, char const* endDoc, + Value* root, std::string* errs) { + bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); + if (errs) { + *errs = reader_.getFormattedErrorMessages(); + } + return ok; + } +}; + +CharReaderBuilder::CharReaderBuilder() +{ + setDefaults(&settings_); +} +CharReaderBuilder::~CharReaderBuilder() +{} +CharReader* CharReaderBuilder::newCharReader() const +{ + bool collectComments = settings_["collectComments"].asBool(); + OurFeatures features = OurFeatures::all(); + features.allowComments_ = settings_["allowComments"].asBool(); + features.strictRoot_ = settings_["strictRoot"].asBool(); + features.allowDroppedNullPlaceholders_ = settings_["allowDroppedNullPlaceholders"].asBool(); + features.allowNumericKeys_ = settings_["allowNumericKeys"].asBool(); + features.allowSingleQuotes_ = settings_["allowSingleQuotes"].asBool(); + features.stackLimit_ = settings_["stackLimit"].asInt(); + features.failIfExtra_ = settings_["failIfExtra"].asBool(); + features.rejectDupKeys_ = settings_["rejectDupKeys"].asBool(); + features.allowSpecialFloats_ = settings_["allowSpecialFloats"].asBool(); + return new OurCharReader(collectComments, features); +} +static void getValidReaderKeys(std::set* valid_keys) +{ + valid_keys->clear(); + valid_keys->insert("collectComments"); + valid_keys->insert("allowComments"); + valid_keys->insert("strictRoot"); + valid_keys->insert("allowDroppedNullPlaceholders"); + valid_keys->insert("allowNumericKeys"); + valid_keys->insert("allowSingleQuotes"); + valid_keys->insert("stackLimit"); + valid_keys->insert("failIfExtra"); + valid_keys->insert("rejectDupKeys"); + valid_keys->insert("allowSpecialFloats"); +} +bool CharReaderBuilder::validate(Json::Value* invalid) const +{ + Json::Value my_invalid; + if (!invalid) invalid = &my_invalid; // so we do not need to test for NULL + Json::Value& inv = *invalid; + std::set valid_keys; + getValidReaderKeys(&valid_keys); + Value::Members keys = settings_.getMemberNames(); + size_t n = keys.size(); + for (size_t i = 0; i < n; ++i) { + std::string const& key = keys[i]; + if (valid_keys.find(key) == valid_keys.end()) { + inv[key] = settings_[key]; + } + } + return 0u == inv.size(); +} +Value& CharReaderBuilder::operator[](std::string key) +{ + return settings_[key]; +} +// static +void CharReaderBuilder::strictMode(Json::Value* settings) +{ +//! [CharReaderBuilderStrictMode] + (*settings)["allowComments"] = false; + (*settings)["strictRoot"] = true; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 1000; + (*settings)["failIfExtra"] = true; + (*settings)["rejectDupKeys"] = true; + (*settings)["allowSpecialFloats"] = false; +//! [CharReaderBuilderStrictMode] +} +// static +void CharReaderBuilder::setDefaults(Json::Value* settings) +{ +//! [CharReaderBuilderDefaults] + (*settings)["collectComments"] = true; + (*settings)["allowComments"] = true; + (*settings)["strictRoot"] = false; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 1000; + (*settings)["failIfExtra"] = false; + (*settings)["rejectDupKeys"] = false; + (*settings)["allowSpecialFloats"] = false; +//! [CharReaderBuilderDefaults] +} + +////////////////////////////////// +// global functions + +bool parseFromStream( + CharReader::Factory const& fact, std::istream& sin, + Value* root, std::string* errs) +{ + std::ostringstream ssin; + ssin << sin.rdbuf(); + std::string doc = ssin.str(); + char const* begin = doc.data(); + char const* end = begin + doc.size(); + // Note that we do not actually need a null-terminator. + CharReaderPtr const reader(fact.newCharReader()); + return reader->parse(begin, end, root, errs); +} + +std::istream& operator>>(std::istream& sin, Value& root) { + CharReaderBuilder b; + std::string errs; + bool ok = parseFromStream(b, sin, &root, &errs); + if (!ok) { + fprintf(stderr, + "Error from reader: %s", + errs.c_str()); + + throwRuntimeError(errs); + } + return sin; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_reader.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_valueiterator.inl +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +// included by json_value.cpp + +namespace Json { + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueIteratorBase +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueIteratorBase::ValueIteratorBase() + : current_(), isNull_(true) { +} + +ValueIteratorBase::ValueIteratorBase( + const Value::ObjectValues::iterator& current) + : current_(current), isNull_(false) {} + +Value& ValueIteratorBase::deref() const { + return current_->second; +} + +void ValueIteratorBase::increment() { + ++current_; +} + +void ValueIteratorBase::decrement() { + --current_; +} + +ValueIteratorBase::difference_type +ValueIteratorBase::computeDistance(const SelfType& other) const { +#ifdef JSON_USE_CPPTL_SMALLMAP + return other.current_ - current_; +#else + // Iterator for null value are initialized using the default + // constructor, which initialize current_ to the default + // std::map::iterator. As begin() and end() are two instance + // of the default std::map::iterator, they can not be compared. + // To allow this, we handle this comparison specifically. + if (isNull_ && other.isNull_) { + return 0; + } + + // Usage of std::distance is not portable (does not compile with Sun Studio 12 + // RogueWave STL, + // which is the one used by default). + // Using a portable hand-made version for non random iterator instead: + // return difference_type( std::distance( current_, other.current_ ) ); + difference_type myDistance = 0; + for (Value::ObjectValues::iterator it = current_; it != other.current_; + ++it) { + ++myDistance; + } + return myDistance; +#endif +} + +bool ValueIteratorBase::isEqual(const SelfType& other) const { + if (isNull_) { + return other.isNull_; + } + return current_ == other.current_; +} + +void ValueIteratorBase::copy(const SelfType& other) { + current_ = other.current_; + isNull_ = other.isNull_; +} + +Value ValueIteratorBase::key() const { + const Value::CZString czstring = (*current_).first; + if (czstring.data()) { + if (czstring.isStaticString()) + return Value(StaticString(czstring.data())); + return Value(czstring.data(), czstring.data() + czstring.length()); + } + return Value(czstring.index()); +} + +UInt ValueIteratorBase::index() const { + const Value::CZString czstring = (*current_).first; + if (!czstring.data()) + return czstring.index(); + return Value::UInt(-1); +} + +std::string ValueIteratorBase::name() const { + char const* keey; + char const* end; + keey = memberName(&end); + if (!keey) return std::string(); + return std::string(keey, end); +} + +char const* ValueIteratorBase::memberName() const { + const char* cname = (*current_).first.data(); + return cname ? cname : ""; +} + +char const* ValueIteratorBase::memberName(char const** end) const { + const char* cname = (*current_).first.data(); + if (!cname) { + *end = NULL; + return NULL; + } + *end = cname + (*current_).first.length(); + return cname; +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueConstIterator +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueConstIterator::ValueConstIterator() {} + +ValueConstIterator::ValueConstIterator( + const Value::ObjectValues::iterator& current) + : ValueIteratorBase(current) {} + +ValueConstIterator::ValueConstIterator(ValueIterator const& other) + : ValueIteratorBase(other) {} + +ValueConstIterator& ValueConstIterator:: +operator=(const ValueIteratorBase& other) { + copy(other); + return *this; +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueIterator +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueIterator::ValueIterator() {} + +ValueIterator::ValueIterator(const Value::ObjectValues::iterator& current) + : ValueIteratorBase(current) {} + +ValueIterator::ValueIterator(const ValueConstIterator& other) + : ValueIteratorBase(other) { + throwRuntimeError("ConstIterator to Iterator should never be allowed."); +} + +ValueIterator::ValueIterator(const ValueIterator& other) + : ValueIteratorBase(other) {} + +ValueIterator& ValueIterator::operator=(const SelfType& other) { + copy(other); + return *this; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_valueiterator.inl +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_value.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#ifdef JSON_USE_CPPTL +#include +#endif +#include // size_t +#include // min() + +#define JSON_ASSERT_UNREACHABLE assert(false) + +namespace Json { + +// This is a walkaround to avoid the static initialization of Value::null. +// kNull must be word-aligned to avoid crashing on ARM. We use an alignment of +// 8 (instead of 4) as a bit of future-proofing. +#if defined(__ARMEL__) +#define ALIGNAS(byte_alignment) __attribute__((aligned(byte_alignment))) +#else +#define ALIGNAS(byte_alignment) +#endif +static const unsigned char ALIGNAS(8) kNull[sizeof(Value)] = { 0 }; +const unsigned char& kNullRef = kNull[0]; +const Value& Value::null = reinterpret_cast(kNullRef); +const Value& Value::nullRef = null; + +const Int Value::minInt = Int(~(UInt(-1) / 2)); +const Int Value::maxInt = Int(UInt(-1) / 2); +const UInt Value::maxUInt = UInt(-1); +#if defined(JSON_HAS_INT64) +const Int64 Value::minInt64 = Int64(~(UInt64(-1) / 2)); +const Int64 Value::maxInt64 = Int64(UInt64(-1) / 2); +const UInt64 Value::maxUInt64 = UInt64(-1); +// The constant is hard-coded because some compiler have trouble +// converting Value::maxUInt64 to a double correctly (AIX/xlC). +// Assumes that UInt64 is a 64 bits integer. +static const double maxUInt64AsDouble = 18446744073709551615.0; +#endif // defined(JSON_HAS_INT64) +const LargestInt Value::minLargestInt = LargestInt(~(LargestUInt(-1) / 2)); +const LargestInt Value::maxLargestInt = LargestInt(LargestUInt(-1) / 2); +const LargestUInt Value::maxLargestUInt = LargestUInt(-1); + +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +template +static inline bool InRange(double d, T min, U max) { + return d >= min && d <= max; +} +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +static inline double integerToDouble(Json::UInt64 value) { + return static_cast(Int64(value / 2)) * 2.0 + Int64(value & 1); +} + +template static inline double integerToDouble(T value) { + return static_cast(value); +} + +template +static inline bool InRange(double d, T min, U max) { + return d >= integerToDouble(min) && d <= integerToDouble(max); +} +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + +/** Duplicates the specified string value. + * @param value Pointer to the string to duplicate. Must be zero-terminated if + * length is "unknown". + * @param length Length of the value. if equals to unknown, then it will be + * computed using strlen(value). + * @return Pointer on the duplicate instance of string. + */ +static inline char* duplicateStringValue(const char* value, + size_t length) { + // Avoid an integer overflow in the call to malloc below by limiting length + // to a sane value. + if (length >= (size_t)Value::maxInt) + length = Value::maxInt - 1; + + char* newString = static_cast(malloc(length + 1)); + if (newString == NULL) { + throwRuntimeError( + "in Json::Value::duplicateStringValue(): " + "Failed to allocate string value buffer"); + } + memcpy(newString, value, length); + newString[length] = 0; + return newString; +} + +/* Record the length as a prefix. + */ +static inline char* duplicateAndPrefixStringValue( + const char* value, + unsigned int length) +{ + // Avoid an integer overflow in the call to malloc below by limiting length + // to a sane value. + JSON_ASSERT_MESSAGE(length <= (unsigned)Value::maxInt - sizeof(unsigned) - 1U, + "in Json::Value::duplicateAndPrefixStringValue(): " + "length too big for prefixing"); + unsigned actualLength = length + static_cast(sizeof(unsigned)) + 1U; + char* newString = static_cast(malloc(actualLength)); + if (newString == 0) { + throwRuntimeError( + "in Json::Value::duplicateAndPrefixStringValue(): " + "Failed to allocate string value buffer"); + } + *reinterpret_cast(newString) = length; + memcpy(newString + sizeof(unsigned), value, length); + newString[actualLength - 1U] = 0; // to avoid buffer over-run accidents by users later + return newString; +} +inline static void decodePrefixedString( + bool isPrefixed, char const* prefixed, + unsigned* length, char const** value) +{ + if (!isPrefixed) { + *length = static_cast(strlen(prefixed)); + *value = prefixed; + } else { + *length = *reinterpret_cast(prefixed); + *value = prefixed + sizeof(unsigned); + } +} +/** Free the string duplicated by duplicateStringValue()/duplicateAndPrefixStringValue(). + */ +static inline void releaseStringValue(char* value) { free(value); } + +} // namespace Json + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ValueInternals... +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +#if !defined(JSON_IS_AMALGAMATION) + +#include "json_valueiterator.inl" +#endif // if !defined(JSON_IS_AMALGAMATION) + +namespace Json { + +Exception::Exception(std::string const& msg) + : msg_(msg) +{} +Exception::~Exception() throw() +{} +char const* Exception::what() const throw() +{ + return msg_.c_str(); +} +RuntimeError::RuntimeError(std::string const& msg) + : Exception(msg) +{} +LogicError::LogicError(std::string const& msg) + : Exception(msg) +{} +void throwRuntimeError(std::string const& msg) +{ + throw RuntimeError(msg); +} +void throwLogicError(std::string const& msg) +{ + throw LogicError(msg); +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::CommentInfo +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +Value::CommentInfo::CommentInfo() : comment_(0) {} + +Value::CommentInfo::~CommentInfo() { + if (comment_) + releaseStringValue(comment_); +} + +void Value::CommentInfo::setComment(const char* text, size_t len) { + if (comment_) { + releaseStringValue(comment_); + comment_ = 0; + } + JSON_ASSERT(text != 0); + JSON_ASSERT_MESSAGE( + text[0] == '\0' || text[0] == '/', + "in Json::Value::setComment(): Comments must start with /"); + // It seems that /**/ style comments are acceptable as well. + comment_ = duplicateStringValue(text, len); +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::CZString +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +// Notes: policy_ indicates if the string was allocated when +// a string is stored. + +Value::CZString::CZString(ArrayIndex aindex) : cstr_(0), index_(aindex) {} + +Value::CZString::CZString(char const* str, unsigned ulength, DuplicationPolicy allocate) + : cstr_(str) { + // allocate != duplicate + storage_.policy_ = allocate & 0x3; + storage_.length_ = ulength & 0x3FFFFFFF; +} + +Value::CZString::CZString(const CZString& other) + : cstr_(other.storage_.policy_ != noDuplication && other.cstr_ != 0 + ? duplicateStringValue(other.cstr_, other.storage_.length_) + : other.cstr_) { + storage_.policy_ = (other.cstr_ + ? (static_cast(other.storage_.policy_) == noDuplication + ? noDuplication : duplicate) + : static_cast(other.storage_.policy_)); + storage_.length_ = other.storage_.length_; +} + +#if JSON_HAS_RVALUE_REFERENCES +Value::CZString::CZString(CZString&& other) + : cstr_(other.cstr_), index_(other.index_) { + other.cstr_ = NULL; +} +#endif + +Value::CZString::~CZString() { + if (cstr_ && storage_.policy_ == duplicate) + releaseStringValue(const_cast(cstr_)); +} + +void Value::CZString::swap(CZString& other) { + std::swap(cstr_, other.cstr_); + std::swap(index_, other.index_); +} + +Value::CZString& Value::CZString::operator=(CZString other) { + swap(other); + return *this; +} + +bool Value::CZString::operator<(const CZString& other) const { + if (!cstr_) return index_ < other.index_; + //return strcmp(cstr_, other.cstr_) < 0; + // Assume both are strings. + unsigned this_len = this->storage_.length_; + unsigned other_len = other.storage_.length_; + unsigned min_len = std::min(this_len, other_len); + int comp = memcmp(this->cstr_, other.cstr_, min_len); + if (comp < 0) return true; + if (comp > 0) return false; + return (this_len < other_len); +} + +bool Value::CZString::operator==(const CZString& other) const { + if (!cstr_) return index_ == other.index_; + //return strcmp(cstr_, other.cstr_) == 0; + // Assume both are strings. + unsigned this_len = this->storage_.length_; + unsigned other_len = other.storage_.length_; + if (this_len != other_len) return false; + int comp = memcmp(this->cstr_, other.cstr_, this_len); + return comp == 0; +} + +ArrayIndex Value::CZString::index() const { return index_; } + +//const char* Value::CZString::c_str() const { return cstr_; } +const char* Value::CZString::data() const { return cstr_; } +unsigned Value::CZString::length() const { return storage_.length_; } +bool Value::CZString::isStaticString() const { return storage_.policy_ == noDuplication; } + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::Value +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +/*! \internal Default constructor initialization must be equivalent to: + * memset( this, 0, sizeof(Value) ) + * This optimization is used in ValueInternalMap fast allocator. + */ +Value::Value(ValueType vtype) { + initBasic(vtype); + switch (vtype) { + case nullValue: + break; + case intValue: + case uintValue: + value_.int_ = 0; + break; + case realValue: + value_.real_ = 0.0; + break; + case stringValue: + value_.string_ = 0; + break; + case arrayValue: + case objectValue: + value_.map_ = new ObjectValues(); + break; + case booleanValue: + value_.bool_ = false; + break; + default: + JSON_ASSERT_UNREACHABLE; + } +} + +Value::Value(Int value) { + initBasic(intValue); + value_.int_ = value; +} + +Value::Value(UInt value) { + initBasic(uintValue); + value_.uint_ = value; +} +#if defined(JSON_HAS_INT64) +Value::Value(Int64 value) { + initBasic(intValue); + value_.int_ = value; +} +Value::Value(UInt64 value) { + initBasic(uintValue); + value_.uint_ = value; +} +#endif // defined(JSON_HAS_INT64) + +Value::Value(double value) { + initBasic(realValue); + value_.real_ = value; +} + +Value::Value(const char* value) { + initBasic(stringValue, true); + value_.string_ = duplicateAndPrefixStringValue(value, static_cast(strlen(value))); +} + +Value::Value(const char* beginValue, const char* endValue) { + initBasic(stringValue, true); + value_.string_ = + duplicateAndPrefixStringValue(beginValue, static_cast(endValue - beginValue)); +} + +Value::Value(const std::string& value) { + initBasic(stringValue, true); + value_.string_ = + duplicateAndPrefixStringValue(value.data(), static_cast(value.length())); +} + +Value::Value(const StaticString& value) { + initBasic(stringValue); + value_.string_ = const_cast(value.c_str()); +} + +#ifdef JSON_USE_CPPTL +Value::Value(const CppTL::ConstString& value) { + initBasic(stringValue, true); + value_.string_ = duplicateAndPrefixStringValue(value, static_cast(value.length())); +} +#endif + +Value::Value(bool value) { + initBasic(booleanValue); + value_.bool_ = value; +} + +Value::Value(Value const& other) + : type_(other.type_), allocated_(false) + , + comments_(0), start_(other.start_), limit_(other.limit_) +{ + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + value_ = other.value_; + break; + case stringValue: + if (other.value_.string_ && other.allocated_) { + unsigned len; + char const* str; + decodePrefixedString(other.allocated_, other.value_.string_, + &len, &str); + value_.string_ = duplicateAndPrefixStringValue(str, len); + allocated_ = true; + } else { + value_.string_ = other.value_.string_; + allocated_ = false; + } + break; + case arrayValue: + case objectValue: + value_.map_ = new ObjectValues(*other.value_.map_); + break; + default: + JSON_ASSERT_UNREACHABLE; + } + if (other.comments_) { + comments_ = new CommentInfo[numberOfCommentPlacement]; + for (int comment = 0; comment < numberOfCommentPlacement; ++comment) { + const CommentInfo& otherComment = other.comments_[comment]; + if (otherComment.comment_) + comments_[comment].setComment( + otherComment.comment_, strlen(otherComment.comment_)); + } + } +} + +#if JSON_HAS_RVALUE_REFERENCES +// Move constructor +Value::Value(Value&& other) { + initBasic(nullValue); + swap(other); +} +#endif + +Value::~Value() { + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + break; + case stringValue: + if (allocated_) + releaseStringValue(value_.string_); + break; + case arrayValue: + case objectValue: + delete value_.map_; + break; + default: + JSON_ASSERT_UNREACHABLE; + } + + if (comments_) + delete[] comments_; +} + +Value& Value::operator=(Value other) { + swap(other); + return *this; +} + +void Value::swapPayload(Value& other) { + ValueType temp = type_; + type_ = other.type_; + other.type_ = temp; + std::swap(value_, other.value_); + int temp2 = allocated_; + allocated_ = other.allocated_; + other.allocated_ = temp2 & 0x1; +} + +void Value::swap(Value& other) { + swapPayload(other); + std::swap(comments_, other.comments_); + std::swap(start_, other.start_); + std::swap(limit_, other.limit_); +} + +ValueType Value::type() const { return type_; } + +int Value::compare(const Value& other) const { + if (*this < other) + return -1; + if (*this > other) + return 1; + return 0; +} + +bool Value::operator<(const Value& other) const { + int typeDelta = type_ - other.type_; + if (typeDelta) + return typeDelta < 0 ? true : false; + switch (type_) { + case nullValue: + return false; + case intValue: + return value_.int_ < other.value_.int_; + case uintValue: + return value_.uint_ < other.value_.uint_; + case realValue: + return value_.real_ < other.value_.real_; + case booleanValue: + return value_.bool_ < other.value_.bool_; + case stringValue: + { + if ((value_.string_ == 0) || (other.value_.string_ == 0)) { + if (other.value_.string_) return true; + else return false; + } + unsigned this_len; + unsigned other_len; + char const* this_str; + char const* other_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + decodePrefixedString(other.allocated_, other.value_.string_, &other_len, &other_str); + unsigned min_len = std::min(this_len, other_len); + int comp = memcmp(this_str, other_str, min_len); + if (comp < 0) return true; + if (comp > 0) return false; + return (this_len < other_len); + } + case arrayValue: + case objectValue: { + int delta = int(value_.map_->size() - other.value_.map_->size()); + if (delta) + return delta < 0; + return (*value_.map_) < (*other.value_.map_); + } + default: + JSON_ASSERT_UNREACHABLE; + } + return false; // unreachable +} + +bool Value::operator<=(const Value& other) const { return !(other < *this); } + +bool Value::operator>=(const Value& other) const { return !(*this < other); } + +bool Value::operator>(const Value& other) const { return other < *this; } + +bool Value::operator==(const Value& other) const { + // if ( type_ != other.type_ ) + // GCC 2.95.3 says: + // attempt to take address of bit-field structure member `Json::Value::type_' + // Beats me, but a temp solves the problem. + int temp = other.type_; + if (type_ != temp) + return false; + switch (type_) { + case nullValue: + return true; + case intValue: + return value_.int_ == other.value_.int_; + case uintValue: + return value_.uint_ == other.value_.uint_; + case realValue: + return value_.real_ == other.value_.real_; + case booleanValue: + return value_.bool_ == other.value_.bool_; + case stringValue: + { + if ((value_.string_ == 0) || (other.value_.string_ == 0)) { + return (value_.string_ == other.value_.string_); + } + unsigned this_len; + unsigned other_len; + char const* this_str; + char const* other_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + decodePrefixedString(other.allocated_, other.value_.string_, &other_len, &other_str); + if (this_len != other_len) return false; + int comp = memcmp(this_str, other_str, this_len); + return comp == 0; + } + case arrayValue: + case objectValue: + return value_.map_->size() == other.value_.map_->size() && + (*value_.map_) == (*other.value_.map_); + default: + JSON_ASSERT_UNREACHABLE; + } + return false; // unreachable +} + +bool Value::operator!=(const Value& other) const { return !(*this == other); } + +const char* Value::asCString() const { + JSON_ASSERT_MESSAGE(type_ == stringValue, + "in Json::Value::asCString(): requires stringValue"); + if (value_.string_ == 0) return 0; + unsigned this_len; + char const* this_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + return this_str; +} + +bool Value::getString(char const** str, char const** cend) const { + if (type_ != stringValue) return false; + if (value_.string_ == 0) return false; + unsigned length; + decodePrefixedString(this->allocated_, this->value_.string_, &length, str); + *cend = *str + length; + return true; +} + +std::string Value::asString() const { + switch (type_) { + case nullValue: + return ""; + case stringValue: + { + if (value_.string_ == 0) return ""; + unsigned this_len; + char const* this_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + return std::string(this_str, this_len); + } + case booleanValue: + return value_.bool_ ? "true" : "false"; + case intValue: + return valueToString(value_.int_); + case uintValue: + return valueToString(value_.uint_); + case realValue: + return valueToString(value_.real_); + default: + JSON_FAIL_MESSAGE("Type is not convertible to string"); + } +} + +#ifdef JSON_USE_CPPTL +CppTL::ConstString Value::asConstString() const { + unsigned len; + char const* str; + decodePrefixedString(allocated_, value_.string_, + &len, &str); + return CppTL::ConstString(str, len); +} +#endif + +Value::Int Value::asInt() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isInt(), "LargestInt out of Int range"); + return Int(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isInt(), "LargestUInt out of Int range"); + return Int(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt, maxInt), + "double out of Int range"); + return Int(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to Int."); +} + +Value::UInt Value::asUInt() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isUInt(), "LargestInt out of UInt range"); + return UInt(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isUInt(), "LargestUInt out of UInt range"); + return UInt(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt), + "double out of UInt range"); + return UInt(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to UInt."); +} + +#if defined(JSON_HAS_INT64) + +Value::Int64 Value::asInt64() const { + switch (type_) { + case intValue: + return Int64(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isInt64(), "LargestUInt out of Int64 range"); + return Int64(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt64, maxInt64), + "double out of Int64 range"); + return Int64(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to Int64."); +} + +Value::UInt64 Value::asUInt64() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isUInt64(), "LargestInt out of UInt64 range"); + return UInt64(value_.int_); + case uintValue: + return UInt64(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt64), + "double out of UInt64 range"); + return UInt64(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to UInt64."); +} +#endif // if defined(JSON_HAS_INT64) + +LargestInt Value::asLargestInt() const { +#if defined(JSON_NO_INT64) + return asInt(); +#else + return asInt64(); +#endif +} + +LargestUInt Value::asLargestUInt() const { +#if defined(JSON_NO_INT64) + return asUInt(); +#else + return asUInt64(); +#endif +} + +double Value::asDouble() const { + switch (type_) { + case intValue: + return static_cast(value_.int_); + case uintValue: +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return static_cast(value_.uint_); +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return integerToDouble(value_.uint_); +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + case realValue: + return value_.real_; + case nullValue: + return 0.0; + case booleanValue: + return value_.bool_ ? 1.0 : 0.0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to double."); +} + +float Value::asFloat() const { + switch (type_) { + case intValue: + return static_cast(value_.int_); + case uintValue: +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return static_cast(value_.uint_); +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return integerToDouble(value_.uint_); +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + case realValue: + return static_cast(value_.real_); + case nullValue: + return 0.0; + case booleanValue: + return value_.bool_ ? 1.0f : 0.0f; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to float."); +} + +bool Value::asBool() const { + switch (type_) { + case booleanValue: + return value_.bool_; + case nullValue: + return false; + case intValue: + return value_.int_ ? true : false; + case uintValue: + return value_.uint_ ? true : false; + case realValue: + // This is kind of strange. Not recommended. + return (value_.real_ != 0.0) ? true : false; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to bool."); +} + +bool Value::isConvertibleTo(ValueType other) const { + switch (other) { + case nullValue: + return (isNumeric() && asDouble() == 0.0) || + (type_ == booleanValue && value_.bool_ == false) || + (type_ == stringValue && asString() == "") || + (type_ == arrayValue && value_.map_->size() == 0) || + (type_ == objectValue && value_.map_->size() == 0) || + type_ == nullValue; + case intValue: + return isInt() || + (type_ == realValue && InRange(value_.real_, minInt, maxInt)) || + type_ == booleanValue || type_ == nullValue; + case uintValue: + return isUInt() || + (type_ == realValue && InRange(value_.real_, 0, maxUInt)) || + type_ == booleanValue || type_ == nullValue; + case realValue: + return isNumeric() || type_ == booleanValue || type_ == nullValue; + case booleanValue: + return isNumeric() || type_ == booleanValue || type_ == nullValue; + case stringValue: + return isNumeric() || type_ == booleanValue || type_ == stringValue || + type_ == nullValue; + case arrayValue: + return type_ == arrayValue || type_ == nullValue; + case objectValue: + return type_ == objectValue || type_ == nullValue; + } + JSON_ASSERT_UNREACHABLE; + return false; +} + +/// Number of values in array or object +ArrayIndex Value::size() const { + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + case stringValue: + return 0; + case arrayValue: // size of the array is highest index + 1 + if (!value_.map_->empty()) { + ObjectValues::const_iterator itLast = value_.map_->end(); + --itLast; + return (*itLast).first.index() + 1; + } + return 0; + case objectValue: + return ArrayIndex(value_.map_->size()); + } + JSON_ASSERT_UNREACHABLE; + return 0; // unreachable; +} + +bool Value::empty() const { + if (isNull() || isArray() || isObject()) + return size() == 0u; + else + return false; +} + +bool Value::operator!() const { return isNull(); } + +void Value::clear() { + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == arrayValue || + type_ == objectValue, + "in Json::Value::clear(): requires complex value"); + start_ = 0; + limit_ = 0; + switch (type_) { + case arrayValue: + case objectValue: + value_.map_->clear(); + break; + default: + break; + } +} + +void Value::resize(ArrayIndex newSize) { + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == arrayValue, + "in Json::Value::resize(): requires arrayValue"); + if (type_ == nullValue) + *this = Value(arrayValue); + ArrayIndex oldSize = size(); + if (newSize == 0) + clear(); + else if (newSize > oldSize) + (*this)[newSize - 1]; + else { + for (ArrayIndex index = newSize; index < oldSize; ++index) { + value_.map_->erase(index); + } + assert(size() == newSize); + } +} + +Value& Value::operator[](ArrayIndex index) { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == arrayValue, + "in Json::Value::operator[](ArrayIndex): requires arrayValue"); + if (type_ == nullValue) + *this = Value(arrayValue); + CZString key(index); + ObjectValues::iterator it = value_.map_->lower_bound(key); + if (it != value_.map_->end() && (*it).first == key) + return (*it).second; + + ObjectValues::value_type defaultValue(key, nullRef); + it = value_.map_->insert(it, defaultValue); + return (*it).second; +} + +Value& Value::operator[](int index) { + JSON_ASSERT_MESSAGE( + index >= 0, + "in Json::Value::operator[](int index): index cannot be negative"); + return (*this)[ArrayIndex(index)]; +} + +const Value& Value::operator[](ArrayIndex index) const { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == arrayValue, + "in Json::Value::operator[](ArrayIndex)const: requires arrayValue"); + if (type_ == nullValue) + return nullRef; + CZString key(index); + ObjectValues::const_iterator it = value_.map_->find(key); + if (it == value_.map_->end()) + return nullRef; + return (*it).second; +} + +const Value& Value::operator[](int index) const { + JSON_ASSERT_MESSAGE( + index >= 0, + "in Json::Value::operator[](int index) const: index cannot be negative"); + return (*this)[ArrayIndex(index)]; +} + +void Value::initBasic(ValueType vtype, bool allocated) { + type_ = vtype; + allocated_ = allocated; + comments_ = 0; + start_ = 0; + limit_ = 0; +} + +// Access an object value by name, create a null member if it does not exist. +// @pre Type of '*this' is object or null. +// @param key is null-terminated. +Value& Value::resolveReference(const char* key) { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::resolveReference(): requires objectValue"); + if (type_ == nullValue) + *this = Value(objectValue); + CZString actualKey( + key, static_cast(strlen(key)), CZString::noDuplication); // NOTE! + ObjectValues::iterator it = value_.map_->lower_bound(actualKey); + if (it != value_.map_->end() && (*it).first == actualKey) + return (*it).second; + + ObjectValues::value_type defaultValue(actualKey, nullRef); + it = value_.map_->insert(it, defaultValue); + Value& value = (*it).second; + return value; +} + +// @param key is not null-terminated. +Value& Value::resolveReference(char const* key, char const* cend) +{ + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::resolveReference(key, end): requires objectValue"); + if (type_ == nullValue) + *this = Value(objectValue); + CZString actualKey( + key, static_cast(cend-key), CZString::duplicateOnCopy); + ObjectValues::iterator it = value_.map_->lower_bound(actualKey); + if (it != value_.map_->end() && (*it).first == actualKey) + return (*it).second; + + ObjectValues::value_type defaultValue(actualKey, nullRef); + it = value_.map_->insert(it, defaultValue); + Value& value = (*it).second; + return value; +} + +Value Value::get(ArrayIndex index, const Value& defaultValue) const { + const Value* value = &((*this)[index]); + return value == &nullRef ? defaultValue : *value; +} + +bool Value::isValidIndex(ArrayIndex index) const { return index < size(); } + +Value const* Value::find(char const* key, char const* cend) const +{ + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::find(key, end, found): requires objectValue or nullValue"); + if (type_ == nullValue) return NULL; + CZString actualKey(key, static_cast(cend-key), CZString::noDuplication); + ObjectValues::const_iterator it = value_.map_->find(actualKey); + if (it == value_.map_->end()) return NULL; + return &(*it).second; +} +const Value& Value::operator[](const char* key) const +{ + Value const* found = find(key, key + strlen(key)); + if (!found) return nullRef; + return *found; +} +Value const& Value::operator[](std::string const& key) const +{ + Value const* found = find(key.data(), key.data() + key.length()); + if (!found) return nullRef; + return *found; +} + +Value& Value::operator[](const char* key) { + return resolveReference(key, key + strlen(key)); +} + +Value& Value::operator[](const std::string& key) { + return resolveReference(key.data(), key.data() + key.length()); +} + +Value& Value::operator[](const StaticString& key) { + return resolveReference(key.c_str()); +} + +#ifdef JSON_USE_CPPTL +Value& Value::operator[](const CppTL::ConstString& key) { + return resolveReference(key.c_str(), key.end_c_str()); +} +Value const& Value::operator[](CppTL::ConstString const& key) const +{ + Value const* found = find(key.c_str(), key.end_c_str()); + if (!found) return nullRef; + return *found; +} +#endif + +Value& Value::append(const Value& value) { return (*this)[size()] = value; } + +Value Value::get(char const* key, char const* cend, Value const& defaultValue) const +{ + Value const* found = find(key, cend); + return !found ? defaultValue : *found; +} +Value Value::get(char const* key, Value const& defaultValue) const +{ + return get(key, key + strlen(key), defaultValue); +} +Value Value::get(std::string const& key, Value const& defaultValue) const +{ + return get(key.data(), key.data() + key.length(), defaultValue); +} + + +bool Value::removeMember(const char* key, const char* cend, Value* removed) +{ + if (type_ != objectValue) { + return false; + } + CZString actualKey(key, static_cast(cend-key), CZString::noDuplication); + ObjectValues::iterator it = value_.map_->find(actualKey); + if (it == value_.map_->end()) + return false; + *removed = it->second; + value_.map_->erase(it); + return true; +} +bool Value::removeMember(const char* key, Value* removed) +{ + return removeMember(key, key + strlen(key), removed); +} +bool Value::removeMember(std::string const& key, Value* removed) +{ + return removeMember(key.data(), key.data() + key.length(), removed); +} +Value Value::removeMember(const char* key) +{ + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == objectValue, + "in Json::Value::removeMember(): requires objectValue"); + if (type_ == nullValue) + return nullRef; + + Value removed; // null + removeMember(key, key + strlen(key), &removed); + return removed; // still null if removeMember() did nothing +} +Value Value::removeMember(const std::string& key) +{ + return removeMember(key.c_str()); +} + +bool Value::removeIndex(ArrayIndex index, Value* removed) { + if (type_ != arrayValue) { + return false; + } + CZString key(index); + ObjectValues::iterator it = value_.map_->find(key); + if (it == value_.map_->end()) { + return false; + } + *removed = it->second; + ArrayIndex oldSize = size(); + // shift left all items left, into the place of the "removed" + for (ArrayIndex i = index; i < (oldSize - 1); ++i){ + CZString keey(i); + (*value_.map_)[keey] = (*this)[i + 1]; + } + // erase the last one ("leftover") + CZString keyLast(oldSize - 1); + ObjectValues::iterator itLast = value_.map_->find(keyLast); + value_.map_->erase(itLast); + return true; +} + +#ifdef JSON_USE_CPPTL +Value Value::get(const CppTL::ConstString& key, + const Value& defaultValue) const { + return get(key.c_str(), key.end_c_str(), defaultValue); +} +#endif + +bool Value::isMember(char const* key, char const* cend) const +{ + Value const* value = find(key, cend); + return NULL != value; +} +bool Value::isMember(char const* key) const +{ + return isMember(key, key + strlen(key)); +} +bool Value::isMember(std::string const& key) const +{ + return isMember(key.data(), key.data() + key.length()); +} + +#ifdef JSON_USE_CPPTL +bool Value::isMember(const CppTL::ConstString& key) const { + return isMember(key.c_str(), key.end_c_str()); +} +#endif + +Value::Members Value::getMemberNames() const { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::getMemberNames(), value must be objectValue"); + if (type_ == nullValue) + return Value::Members(); + Members members; + members.reserve(value_.map_->size()); + ObjectValues::const_iterator it = value_.map_->begin(); + ObjectValues::const_iterator itEnd = value_.map_->end(); + for (; it != itEnd; ++it) { + members.push_back(std::string((*it).first.data(), + (*it).first.length())); + } + return members; +} +// +//# ifdef JSON_USE_CPPTL +// EnumMemberNames +// Value::enumMemberNames() const +//{ +// if ( type_ == objectValue ) +// { +// return CppTL::Enum::any( CppTL::Enum::transform( +// CppTL::Enum::keys( *(value_.map_), CppTL::Type() ), +// MemberNamesTransform() ) ); +// } +// return EnumMemberNames(); +//} +// +// +// EnumValues +// Value::enumValues() const +//{ +// if ( type_ == objectValue || type_ == arrayValue ) +// return CppTL::Enum::anyValues( *(value_.map_), +// CppTL::Type() ); +// return EnumValues(); +//} +// +//# endif + +static bool IsIntegral(double d) { + double integral_part; + return modf(d, &integral_part) == 0.0; +} + +bool Value::isNull() const { return type_ == nullValue; } + +bool Value::isBool() const { return type_ == booleanValue; } + +bool Value::isInt() const { + switch (type_) { + case intValue: + return value_.int_ >= minInt && value_.int_ <= maxInt; + case uintValue: + return value_.uint_ <= UInt(maxInt); + case realValue: + return value_.real_ >= minInt && value_.real_ <= maxInt && + IsIntegral(value_.real_); + default: + break; + } + return false; +} + +bool Value::isUInt() const { + switch (type_) { + case intValue: + return value_.int_ >= 0 && LargestUInt(value_.int_) <= LargestUInt(maxUInt); + case uintValue: + return value_.uint_ <= maxUInt; + case realValue: + return value_.real_ >= 0 && value_.real_ <= maxUInt && + IsIntegral(value_.real_); + default: + break; + } + return false; +} + +bool Value::isInt64() const { +#if defined(JSON_HAS_INT64) + switch (type_) { + case intValue: + return true; + case uintValue: + return value_.uint_ <= UInt64(maxInt64); + case realValue: + // Note that maxInt64 (= 2^63 - 1) is not exactly representable as a + // double, so double(maxInt64) will be rounded up to 2^63. Therefore we + // require the value to be strictly less than the limit. + return value_.real_ >= double(minInt64) && + value_.real_ < double(maxInt64) && IsIntegral(value_.real_); + default: + break; + } +#endif // JSON_HAS_INT64 + return false; +} + +bool Value::isUInt64() const { +#if defined(JSON_HAS_INT64) + switch (type_) { + case intValue: + return value_.int_ >= 0; + case uintValue: + return true; + case realValue: + // Note that maxUInt64 (= 2^64 - 1) is not exactly representable as a + // double, so double(maxUInt64) will be rounded up to 2^64. Therefore we + // require the value to be strictly less than the limit. + return value_.real_ >= 0 && value_.real_ < maxUInt64AsDouble && + IsIntegral(value_.real_); + default: + break; + } +#endif // JSON_HAS_INT64 + return false; +} + +bool Value::isIntegral() const { +#if defined(JSON_HAS_INT64) + return isInt64() || isUInt64(); +#else + return isInt() || isUInt(); +#endif +} + +bool Value::isDouble() const { return type_ == realValue || isIntegral(); } + +bool Value::isNumeric() const { return isIntegral() || isDouble(); } + +bool Value::isString() const { return type_ == stringValue; } + +bool Value::isArray() const { return type_ == arrayValue; } + +bool Value::isObject() const { return type_ == objectValue; } + +void Value::setComment(const char* comment, size_t len, CommentPlacement placement) { + if (!comments_) + comments_ = new CommentInfo[numberOfCommentPlacement]; + if ((len > 0) && (comment[len-1] == '\n')) { + // Always discard trailing newline, to aid indentation. + len -= 1; + } + comments_[placement].setComment(comment, len); +} + +void Value::setComment(const char* comment, CommentPlacement placement) { + setComment(comment, strlen(comment), placement); +} + +void Value::setComment(const std::string& comment, CommentPlacement placement) { + setComment(comment.c_str(), comment.length(), placement); +} + +bool Value::hasComment(CommentPlacement placement) const { + return comments_ != 0 && comments_[placement].comment_ != 0; +} + +std::string Value::getComment(CommentPlacement placement) const { + if (hasComment(placement)) + return comments_[placement].comment_; + return ""; +} + +void Value::setOffsetStart(size_t start) { start_ = start; } + +void Value::setOffsetLimit(size_t limit) { limit_ = limit; } + +size_t Value::getOffsetStart() const { return start_; } + +size_t Value::getOffsetLimit() const { return limit_; } + +std::string Value::toStyledString() const { + StyledWriter writer; + return writer.write(*this); +} + +Value::const_iterator Value::begin() const { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return const_iterator(value_.map_->begin()); + break; + default: + break; + } + return const_iterator(); +} + +Value::const_iterator Value::end() const { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return const_iterator(value_.map_->end()); + break; + default: + break; + } + return const_iterator(); +} + +Value::iterator Value::begin() { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return iterator(value_.map_->begin()); + break; + default: + break; + } + return iterator(); +} + +Value::iterator Value::end() { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return iterator(value_.map_->end()); + break; + default: + break; + } + return iterator(); +} + +// class PathArgument +// ////////////////////////////////////////////////////////////////// + +PathArgument::PathArgument() : key_(), index_(), kind_(kindNone) {} + +PathArgument::PathArgument(ArrayIndex index) + : key_(), index_(index), kind_(kindIndex) {} + +PathArgument::PathArgument(const char* key) + : key_(key), index_(), kind_(kindKey) {} + +PathArgument::PathArgument(const std::string& key) + : key_(key.c_str()), index_(), kind_(kindKey) {} + +// class Path +// ////////////////////////////////////////////////////////////////// + +Path::Path(const std::string& path, + const PathArgument& a1, + const PathArgument& a2, + const PathArgument& a3, + const PathArgument& a4, + const PathArgument& a5) { + InArgs in; + in.push_back(&a1); + in.push_back(&a2); + in.push_back(&a3); + in.push_back(&a4); + in.push_back(&a5); + makePath(path, in); +} + +void Path::makePath(const std::string& path, const InArgs& in) { + const char* current = path.c_str(); + const char* end = current + path.length(); + InArgs::const_iterator itInArg = in.begin(); + while (current != end) { + if (*current == '[') { + ++current; + if (*current == '%') + addPathInArg(path, in, itInArg, PathArgument::kindIndex); + else { + ArrayIndex index = 0; + for (; current != end && *current >= '0' && *current <= '9'; ++current) + index = index * 10 + ArrayIndex(*current - '0'); + args_.push_back(index); + } + if (current == end || *current++ != ']') + invalidPath(path, int(current - path.c_str())); + } else if (*current == '%') { + addPathInArg(path, in, itInArg, PathArgument::kindKey); + ++current; + } else if (*current == '.') { + ++current; + } else { + const char* beginName = current; + while (current != end && !strchr("[.", *current)) + ++current; + args_.push_back(std::string(beginName, current)); + } + } +} + +void Path::addPathInArg(const std::string& /*path*/, + const InArgs& in, + InArgs::const_iterator& itInArg, + PathArgument::Kind kind) { + if (itInArg == in.end()) { + // Error: missing argument %d + } else if ((*itInArg)->kind_ != kind) { + // Error: bad argument type + } else { + args_.push_back(**itInArg); + } +} + +void Path::invalidPath(const std::string& /*path*/, int /*location*/) { + // Error: invalid path. +} + +const Value& Path::resolve(const Value& root) const { + const Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray() || !node->isValidIndex(arg.index_)) { + // Error: unable to resolve path (array value expected at position... + } + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) { + // Error: unable to resolve path (object value expected at position...) + } + node = &((*node)[arg.key_]); + if (node == &Value::nullRef) { + // Error: unable to resolve path (object has no member named '' at + // position...) + } + } + } + return *node; +} + +Value Path::resolve(const Value& root, const Value& defaultValue) const { + const Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray() || !node->isValidIndex(arg.index_)) + return defaultValue; + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) + return defaultValue; + node = &((*node)[arg.key_]); + if (node == &Value::nullRef) + return defaultValue; + } + } + return *node; +} + +Value& Path::make(Value& root) const { + Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray()) { + // Error: node is not an array at position ... + } + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) { + // Error: node is not an object at position... + } + node = &((*node)[arg.key_]); + } + } + return *node; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_value.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_writer.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include "json_tool.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) && _MSC_VER >= 1200 && _MSC_VER < 1800 // Between VC++ 6.0 and VC++ 11.0 +#include +#define isfinite _finite +#elif defined(__sun) && defined(__SVR4) //Solaris +#if !defined(isfinite) +#include +#define isfinite finite +#endif +#elif defined(_AIX) +#if !defined(isfinite) +#include +#define isfinite finite +#endif +#elif defined(__hpux) +#if !defined(isfinite) +#if defined(__ia64) && !defined(finite) +#define isfinite(x) ((sizeof(x) == sizeof(float) ? \ + _Isfinitef(x) : _IsFinite(x))) +#else +#include +#define isfinite finite +#endif +#endif +#else +#include +#if !(defined(__QNXNTO__)) // QNX already defines isfinite +#define isfinite std::isfinite +#endif +#endif + +#if defined(_MSC_VER) +#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above +#define snprintf sprintf_s +#elif _MSC_VER >= 1900 // VC++ 14.0 and above +#define snprintf std::snprintf +#else +#define snprintf _snprintf +#endif +#elif defined(__ANDROID__) || defined(__QNXNTO__) +#define snprintf snprintf +#elif __cplusplus >= 201103L +#define snprintf std::snprintf +#endif + +#if defined(__BORLANDC__) +#include +#define isfinite _finite +#define snprintf _snprintf +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 // VC++ 8.0 +// Disable warning about strdup being deprecated. +#pragma warning(disable : 4996) +#endif + +namespace Json { + +#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) +typedef std::unique_ptr StreamWriterPtr; +#else +typedef std::auto_ptr StreamWriterPtr; +#endif + +static bool containsControlCharacter(const char* str) { + while (*str) { + if (isControlCharacter(*(str++))) + return true; + } + return false; +} + +static bool containsControlCharacter0(const char* str, unsigned len) { + char const* end = str + len; + while (end != str) { + if (isControlCharacter(*str) || 0==*str) + return true; + ++str; + } + return false; +} + +std::string valueToString(LargestInt value) { + UIntToStringBuffer buffer; + char* current = buffer + sizeof(buffer); + if (value == Value::minLargestInt) { + uintToString(LargestUInt(Value::maxLargestInt) + 1, current); + *--current = '-'; + } else if (value < 0) { + uintToString(LargestUInt(-value), current); + *--current = '-'; + } else { + uintToString(LargestUInt(value), current); + } + assert(current >= buffer); + return current; +} + +std::string valueToString(LargestUInt value) { + UIntToStringBuffer buffer; + char* current = buffer + sizeof(buffer); + uintToString(value, current); + assert(current >= buffer); + return current; +} + +#if defined(JSON_HAS_INT64) + +std::string valueToString(Int value) { + return valueToString(LargestInt(value)); +} + +std::string valueToString(UInt value) { + return valueToString(LargestUInt(value)); +} + +#endif // # if defined(JSON_HAS_INT64) + +std::string valueToString(double value, bool useSpecialFloats, unsigned int precision) { + // Allocate a buffer that is more than large enough to store the 16 digits of + // precision requested below. + char buffer[32]; + int len = -1; + + char formatString[6]; + sprintf(formatString, "%%.%dg", precision); + + // Print into the buffer. We need not request the alternative representation + // that always has a decimal point because JSON doesn't distingish the + // concepts of reals and integers. + if (isfinite(value)) { + len = snprintf(buffer, sizeof(buffer), formatString, value); + } else { + // IEEE standard states that NaN values will not compare to themselves + if (value != value) { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "NaN" : "null"); + } else if (value < 0) { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "-Infinity" : "-1e+9999"); + } else { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "Infinity" : "1e+9999"); + } + // For those, we do not need to call fixNumLoc, but it is fast. + } + assert(len >= 0); + fixNumericLocale(buffer, buffer + len); + return buffer; +} + +std::string valueToString(double value) { return valueToString(value, false, 17); } + +std::string valueToString(bool value) { return value ? "true" : "false"; } + +std::string valueToQuotedString(const char* value) { + if (value == NULL) + return ""; + // Not sure how to handle unicode... + if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && + !containsControlCharacter(value)) + return std::string("\"") + value + "\""; + // We have to walk value and escape any special characters. + // Appending to std::string is not efficient, but this should be rare. + // (Note: forward slashes are *not* rare, but I am not escaping them.) + std::string::size_type maxsize = + strlen(value) * 2 + 3; // allescaped+quotes+NULL + std::string result; + result.reserve(maxsize); // to avoid lots of mallocs + result += "\""; + for (const char* c = value; *c != 0; ++c) { + switch (*c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + // case '/': + // Even though \/ is considered a legal escape in JSON, a bare + // slash is also legal, so I see no reason to escape it. + // (I hope I am not misunderstanding something. + // blep notes: actually escaping \/ may be useful in javascript to avoid (*c); + result += oss.str(); + } else { + result += *c; + } + break; + } + } + result += "\""; + return result; +} + +// https://github.com/upcaste/upcaste/blob/master/src/upcore/src/cstring/strnpbrk.cpp +static char const* strnpbrk(char const* s, char const* accept, size_t n) { + assert((s || !n) && accept); + + char const* const end = s + n; + for (char const* cur = s; cur < end; ++cur) { + int const c = *cur; + for (char const* a = accept; *a; ++a) { + if (*a == c) { + return cur; + } + } + } + return NULL; +} +static std::string valueToQuotedStringN(const char* value, unsigned length) { + if (value == NULL) + return ""; + // Not sure how to handle unicode... + if (strnpbrk(value, "\"\\\b\f\n\r\t", length) == NULL && + !containsControlCharacter0(value, length)) + return std::string("\"") + value + "\""; + // We have to walk value and escape any special characters. + // Appending to std::string is not efficient, but this should be rare. + // (Note: forward slashes are *not* rare, but I am not escaping them.) + std::string::size_type maxsize = + length * 2 + 3; // allescaped+quotes+NULL + std::string result; + result.reserve(maxsize); // to avoid lots of mallocs + result += "\""; + char const* end = value + length; + for (const char* c = value; c != end; ++c) { + switch (*c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + // case '/': + // Even though \/ is considered a legal escape in JSON, a bare + // slash is also legal, so I see no reason to escape it. + // (I hope I am not misunderstanding something.) + // blep notes: actually escaping \/ may be useful in javascript to avoid (*c); + result += oss.str(); + } else { + result += *c; + } + break; + } + } + result += "\""; + return result; +} + +// Class Writer +// ////////////////////////////////////////////////////////////////// +Writer::~Writer() {} + +// Class FastWriter +// ////////////////////////////////////////////////////////////////// + +FastWriter::FastWriter() + : yamlCompatiblityEnabled_(false), dropNullPlaceholders_(false), + omitEndingLineFeed_(false) {} + +void FastWriter::enableYAMLCompatibility() { yamlCompatiblityEnabled_ = true; } + +void FastWriter::dropNullPlaceholders() { dropNullPlaceholders_ = true; } + +void FastWriter::omitEndingLineFeed() { omitEndingLineFeed_ = true; } + +std::string FastWriter::write(const Value& root) { + document_ = ""; + writeValue(root); + if (!omitEndingLineFeed_) + document_ += "\n"; + return document_; +} + +void FastWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + if (!dropNullPlaceholders_) + document_ += "null"; + break; + case intValue: + document_ += valueToString(value.asLargestInt()); + break; + case uintValue: + document_ += valueToString(value.asLargestUInt()); + break; + case realValue: + document_ += valueToString(value.asDouble()); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) document_ += valueToQuotedStringN(str, static_cast(end-str)); + break; + } + case booleanValue: + document_ += valueToString(value.asBool()); + break; + case arrayValue: { + document_ += '['; + int size = value.size(); + for (int index = 0; index < size; ++index) { + if (index > 0) + document_ += ','; + writeValue(value[index]); + } + document_ += ']'; + } break; + case objectValue: { + Value::Members members(value.getMemberNames()); + document_ += '{'; + for (Value::Members::iterator it = members.begin(); it != members.end(); + ++it) { + const std::string& name = *it; + if (it != members.begin()) + document_ += ','; + document_ += valueToQuotedStringN(name.data(), static_cast(name.length())); + document_ += yamlCompatiblityEnabled_ ? ": " : ":"; + writeValue(value[name]); + } + document_ += '}'; + } break; + } +} + +// Class StyledWriter +// ////////////////////////////////////////////////////////////////// + +StyledWriter::StyledWriter() + : rightMargin_(74), indentSize_(3), addChildValues_() {} + +std::string StyledWriter::write(const Value& root) { + document_ = ""; + addChildValues_ = false; + indentString_ = ""; + writeCommentBeforeValue(root); + writeValue(root); + writeCommentAfterValueOnSameLine(root); + document_ += "\n"; + return document_; +} + +void StyledWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + pushValue("null"); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble())); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + const std::string& name = *it; + const Value& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedString(name.c_str())); + document_ += " : "; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + document_ += ','; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void StyledWriter::writeArrayValue(const Value& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isArrayMultiLine = isMultineArray(value); + if (isArrayMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + const Value& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + writeIndent(); + writeValue(childValue); + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + document_ += ','; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + document_ += "[ "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + document_ += ", "; + document_ += childValues_[index]; + } + document_ += " ]"; + } + } +} + +bool StyledWriter::isMultineArray(const Value& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + const Value& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void StyledWriter::pushValue(const std::string& value) { + if (addChildValues_) + childValues_.push_back(value); + else + document_ += value; +} + +void StyledWriter::writeIndent() { + if (!document_.empty()) { + char last = document_[document_.length() - 1]; + if (last == ' ') // already indented + return; + if (last != '\n') // Comments may add new-line + document_ += '\n'; + } + document_ += indentString_; +} + +void StyledWriter::writeWithIndent(const std::string& value) { + writeIndent(); + document_ += value; +} + +void StyledWriter::indent() { indentString_ += std::string(indentSize_, ' '); } + +void StyledWriter::unindent() { + assert(int(indentString_.size()) >= indentSize_); + indentString_.resize(indentString_.size() - indentSize_); +} + +void StyledWriter::writeCommentBeforeValue(const Value& root) { + if (!root.hasComment(commentBefore)) + return; + + document_ += "\n"; + writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + document_ += *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + writeIndent(); + ++iter; + } + + // Comments are stripped of trailing newlines, so add one here + document_ += "\n"; +} + +void StyledWriter::writeCommentAfterValueOnSameLine(const Value& root) { + if (root.hasComment(commentAfterOnSameLine)) + document_ += " " + root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + document_ += "\n"; + document_ += root.getComment(commentAfter); + document_ += "\n"; + } +} + +bool StyledWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +// Class StyledStreamWriter +// ////////////////////////////////////////////////////////////////// + +StyledStreamWriter::StyledStreamWriter(std::string indentation) + : document_(NULL), rightMargin_(74), indentation_(indentation), + addChildValues_() {} + +void StyledStreamWriter::write(std::ostream& out, const Value& root) { + document_ = &out; + addChildValues_ = false; + indentString_ = ""; + indented_ = true; + writeCommentBeforeValue(root); + if (!indented_) writeIndent(); + indented_ = true; + writeValue(root); + writeCommentAfterValueOnSameLine(root); + *document_ << "\n"; + document_ = NULL; // Forget the stream, for safety. +} + +void StyledStreamWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + pushValue("null"); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble())); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + const std::string& name = *it; + const Value& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedString(name.c_str())); + *document_ << " : "; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *document_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void StyledStreamWriter::writeArrayValue(const Value& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isArrayMultiLine = isMultineArray(value); + if (isArrayMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + const Value& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + if (!indented_) writeIndent(); + indented_ = true; + writeValue(childValue); + indented_ = false; + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *document_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + *document_ << "[ "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + *document_ << ", "; + *document_ << childValues_[index]; + } + *document_ << " ]"; + } + } +} + +bool StyledStreamWriter::isMultineArray(const Value& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + const Value& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void StyledStreamWriter::pushValue(const std::string& value) { + if (addChildValues_) + childValues_.push_back(value); + else + *document_ << value; +} + +void StyledStreamWriter::writeIndent() { + // blep intended this to look at the so-far-written string + // to determine whether we are already indented, but + // with a stream we cannot do that. So we rely on some saved state. + // The caller checks indented_. + *document_ << '\n' << indentString_; +} + +void StyledStreamWriter::writeWithIndent(const std::string& value) { + if (!indented_) writeIndent(); + *document_ << value; + indented_ = false; +} + +void StyledStreamWriter::indent() { indentString_ += indentation_; } + +void StyledStreamWriter::unindent() { + assert(indentString_.size() >= indentation_.size()); + indentString_.resize(indentString_.size() - indentation_.size()); +} + +void StyledStreamWriter::writeCommentBeforeValue(const Value& root) { + if (!root.hasComment(commentBefore)) + return; + + if (!indented_) writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + *document_ << *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + // writeIndent(); // would include newline + *document_ << indentString_; + ++iter; + } + indented_ = false; +} + +void StyledStreamWriter::writeCommentAfterValueOnSameLine(const Value& root) { + if (root.hasComment(commentAfterOnSameLine)) + *document_ << ' ' << root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + writeIndent(); + *document_ << root.getComment(commentAfter); + } + indented_ = false; +} + +bool StyledStreamWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +////////////////////////// +// BuiltStyledStreamWriter + +/// Scoped enums are not available until C++11. +struct CommentStyle { + /// Decide whether to write comments. + enum Enum { + None, ///< Drop all comments. + Most, ///< Recover odd behavior of previous versions (not implemented yet). + All ///< Keep all comments. + }; +}; + +struct BuiltStyledStreamWriter : public StreamWriter +{ + BuiltStyledStreamWriter( + std::string const& indentation, + CommentStyle::Enum cs, + std::string const& colonSymbol, + std::string const& nullSymbol, + std::string const& endingLineFeedSymbol, + bool useSpecialFloats, + unsigned int precision); + int write(Value const& root, std::ostream* sout) ; +private: + void writeValue(Value const& value); + void writeArrayValue(Value const& value); + bool isMultineArray(Value const& value); + void pushValue(std::string const& value); + void writeIndent(); + void writeWithIndent(std::string const& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(Value const& root); + void writeCommentAfterValueOnSameLine(Value const& root); + static bool hasCommentForValue(const Value& value); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::string indentString_; + int rightMargin_; + std::string indentation_; + CommentStyle::Enum cs_; + std::string colonSymbol_; + std::string nullSymbol_; + std::string endingLineFeedSymbol_; + bool addChildValues_ : 1; + bool indented_ : 1; + bool useSpecialFloats_ : 1; + unsigned int precision_; +}; +BuiltStyledStreamWriter::BuiltStyledStreamWriter( + std::string const& indentation, + CommentStyle::Enum cs, + std::string const& colonSymbol, + std::string const& nullSymbol, + std::string const& endingLineFeedSymbol, + bool useSpecialFloats, + unsigned int precision) + : rightMargin_(74) + , indentation_(indentation) + , cs_(cs) + , colonSymbol_(colonSymbol) + , nullSymbol_(nullSymbol) + , endingLineFeedSymbol_(endingLineFeedSymbol) + , addChildValues_(false) + , indented_(false) + , useSpecialFloats_(useSpecialFloats) + , precision_(precision) +{ +} +int BuiltStyledStreamWriter::write(Value const& root, std::ostream* sout) +{ + sout_ = sout; + addChildValues_ = false; + indented_ = true; + indentString_ = ""; + writeCommentBeforeValue(root); + if (!indented_) writeIndent(); + indented_ = true; + writeValue(root); + writeCommentAfterValueOnSameLine(root); + *sout_ << endingLineFeedSymbol_; + sout_ = NULL; + return 0; +} +void BuiltStyledStreamWriter::writeValue(Value const& value) { + switch (value.type()) { + case nullValue: + pushValue(nullSymbol_); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble(), useSpecialFloats_, precision_)); + break; + case stringValue: + { + // Is NULL is possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + std::string const& name = *it; + Value const& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedStringN(name.data(), static_cast(name.length()))); + *sout_ << colonSymbol_; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *sout_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void BuiltStyledStreamWriter::writeArrayValue(Value const& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isMultiLine = (cs_ == CommentStyle::All) || isMultineArray(value); + if (isMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + Value const& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + if (!indented_) writeIndent(); + indented_ = true; + writeValue(childValue); + indented_ = false; + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *sout_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + *sout_ << "["; + if (!indentation_.empty()) *sout_ << " "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + *sout_ << ", "; + *sout_ << childValues_[index]; + } + if (!indentation_.empty()) *sout_ << " "; + *sout_ << "]"; + } + } +} + +bool BuiltStyledStreamWriter::isMultineArray(Value const& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + Value const& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void BuiltStyledStreamWriter::pushValue(std::string const& value) { + if (addChildValues_) + childValues_.push_back(value); + else + *sout_ << value; +} + +void BuiltStyledStreamWriter::writeIndent() { + // blep intended this to look at the so-far-written string + // to determine whether we are already indented, but + // with a stream we cannot do that. So we rely on some saved state. + // The caller checks indented_. + + if (!indentation_.empty()) { + // In this case, drop newlines too. + *sout_ << '\n' << indentString_; + } +} + +void BuiltStyledStreamWriter::writeWithIndent(std::string const& value) { + if (!indented_) writeIndent(); + *sout_ << value; + indented_ = false; +} + +void BuiltStyledStreamWriter::indent() { indentString_ += indentation_; } + +void BuiltStyledStreamWriter::unindent() { + assert(indentString_.size() >= indentation_.size()); + indentString_.resize(indentString_.size() - indentation_.size()); +} + +void BuiltStyledStreamWriter::writeCommentBeforeValue(Value const& root) { + if (cs_ == CommentStyle::None) return; + if (!root.hasComment(commentBefore)) + return; + + if (!indented_) writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + *sout_ << *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + // writeIndent(); // would write extra newline + *sout_ << indentString_; + ++iter; + } + indented_ = false; +} + +void BuiltStyledStreamWriter::writeCommentAfterValueOnSameLine(Value const& root) { + if (cs_ == CommentStyle::None) return; + if (root.hasComment(commentAfterOnSameLine)) + *sout_ << " " + root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + writeIndent(); + *sout_ << root.getComment(commentAfter); + } +} + +// static +bool BuiltStyledStreamWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +/////////////// +// StreamWriter + +StreamWriter::StreamWriter() + : sout_(NULL) +{ +} +StreamWriter::~StreamWriter() +{ +} +StreamWriter::Factory::~Factory() +{} +StreamWriterBuilder::StreamWriterBuilder() +{ + setDefaults(&settings_); +} +StreamWriterBuilder::~StreamWriterBuilder() +{} +StreamWriter* StreamWriterBuilder::newStreamWriter() const +{ + std::string indentation = settings_["indentation"].asString(); + std::string cs_str = settings_["commentStyle"].asString(); + bool eyc = settings_["enableYAMLCompatibility"].asBool(); + bool dnp = settings_["dropNullPlaceholders"].asBool(); + bool usf = settings_["useSpecialFloats"].asBool(); + unsigned int pre = settings_["precision"].asUInt(); + CommentStyle::Enum cs = CommentStyle::All; + if (cs_str == "All") { + cs = CommentStyle::All; + } else if (cs_str == "None") { + cs = CommentStyle::None; + } else { + throwRuntimeError("commentStyle must be 'All' or 'None'"); + } + std::string colonSymbol = " : "; + if (eyc) { + colonSymbol = ": "; + } else if (indentation.empty()) { + colonSymbol = ":"; + } + std::string nullSymbol = "null"; + if (dnp) { + nullSymbol = ""; + } + if (pre > 17) pre = 17; + std::string endingLineFeedSymbol = ""; + return new BuiltStyledStreamWriter( + indentation, cs, + colonSymbol, nullSymbol, endingLineFeedSymbol, usf, pre); +} +static void getValidWriterKeys(std::set* valid_keys) +{ + valid_keys->clear(); + valid_keys->insert("indentation"); + valid_keys->insert("commentStyle"); + valid_keys->insert("enableYAMLCompatibility"); + valid_keys->insert("dropNullPlaceholders"); + valid_keys->insert("useSpecialFloats"); + valid_keys->insert("precision"); +} +bool StreamWriterBuilder::validate(Json::Value* invalid) const +{ + Json::Value my_invalid; + if (!invalid) invalid = &my_invalid; // so we do not need to test for NULL + Json::Value& inv = *invalid; + std::set valid_keys; + getValidWriterKeys(&valid_keys); + Value::Members keys = settings_.getMemberNames(); + size_t n = keys.size(); + for (size_t i = 0; i < n; ++i) { + std::string const& key = keys[i]; + if (valid_keys.find(key) == valid_keys.end()) { + inv[key] = settings_[key]; + } + } + return 0u == inv.size(); +} +Value& StreamWriterBuilder::operator[](std::string key) +{ + return settings_[key]; +} +// static +void StreamWriterBuilder::setDefaults(Json::Value* settings) +{ + //! [StreamWriterBuilderDefaults] + (*settings)["commentStyle"] = "All"; + (*settings)["indentation"] = "\t"; + (*settings)["enableYAMLCompatibility"] = false; + (*settings)["dropNullPlaceholders"] = false; + (*settings)["useSpecialFloats"] = false; + (*settings)["precision"] = 17; + //! [StreamWriterBuilderDefaults] +} + +std::string writeString(StreamWriter::Factory const& builder, Value const& root) { + std::ostringstream sout; + StreamWriterPtr const writer(builder.newStreamWriter()); + writer->write(root, &sout); + return sout.str(); +} + +std::ostream& operator<<(std::ostream& sout, Value const& root) { + StreamWriterBuilder builder; + StreamWriterPtr const writer(builder.newStreamWriter()); + writer->write(root, &sout); + return sout; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_writer.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp new file mode 100755 index 000000000..b28be6675 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -0,0 +1,1483 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netcdf_utils.h" +#include "string_utils.h" + +#define ID_LEN 10 + +using namespace std; + +enum { mergeOp, invertOp, preserveOp }; + +size_t nCells, nVertices, nEdges, vertexDegree, maxEdges; +bool spherical, periodic; +bool cullMasks = false; +double sphere_radius, xPeriod, yPeriod; +string in_history = ""; +string in_file_id = ""; +string in_parent_id = ""; +string in_mesh_spec = "1.0"; +bool outputMap = false; + +// Connectivity and location information {{{ + +vector cullCell; +vector cellMap; +vector vertexMap; +vector edgeMap; +vector< vector > verticesOnEdge; +vector< vector > cellsOnEdge; +vector< vector > cellsOnVertex; +vector areaCell; + +// }}} + +/* Input/Marking Functions {{{ */ +int readGridInput(const string inputFilename); +int mergeCellMasks(const string masksFilename, const int maskOp); +int markCells(); +int markVertices(); +int markEdges(); +/*}}}*/ + +/* Mapping/Output functions {{{*/ +int outputGridDimensions(const string outputFilename); +int outputGridAttributes(const string inputFilename, const string outputFilename); +int mapAndOutputGridCoordinates(const string inputFilename, const string outputFilename); +int mapAndOutputCellFields(const string inputFilename, const string outputPath, + const string outputFilename); +int mapAndOutputEdgeFields(const string inputFilename, const string outputFilename); +int mapAndOutputVertexFields(const string inputFilename, const string outputFilename); +int outputCellMap(const string outputPath); +/*}}}*/ + +void print_usage(){/*{{{*/ + cout << endl << endl; + cout << "Usage:" << endl; + cout << "\tMpasCellCuller.x [input_name] [output_name] [[-m/-i/-p] masks_name] [-c]" << endl; + cout << endl; + cout << "\t\tinput_name:" << endl; + cout << "\t\t\tThis argument specifies the input MPAS mesh." << endl; + cout << "\t\toutput_name:" << endl; + cout << "\t\t\tThis argument specifies the output culled MPAS mesh." << endl; + cout << "\t\t\tIf not specified, it defaults to culled_mesh.nc, but" << endl; + cout << "\t\t\tit is required if additional arguments are specified." << endl; + cout << "\t\t-m/-i/-p:" << endl; + cout << "\t\t\tThese arguments control how a set of masks is used when" << endl; + cout << "\t\t\tculling a mesh." << endl; + cout << "\t\t\tThe -m argument applies a mask to cull based on (i.e." << endl; + cout << "\t\t\twhere the mask is 1, the mesh will be culled)." << endl; + cout << "\t\t\tThe -i argument applies the inverse mask to cull based" << endl; + cout << "\t\t\ton (i.e. where the mask is 0, the mesh will be" << endl; + cout << "\t\t\tculled)." << endl; + cout << "\t\t\tThe -p argument forces any marked cells to not be" << endl; + cout << "\t\t\tculled." << endl; + cout << "\t\t\tIf this argument is specified, the masks_name argument" << endl; + cout << "\t\t\tis required" << endl; + cout << "\t\t-c:" << endl; + cout << "\t\t\tOutput the mapping from old to new mesh (cellMap) in" << endl; + cout << "\t\t\t\tcellMapForward.txt, " << endl; + cout << "\t\t\tand output the reverse mapping from new to old mesh in" << endl; + cout << "\t\t\t\tcellMapBackward.txt." << endl; +}/*}}}*/ + +string gen_random(const int len); + +int main ( int argc, char *argv[] ) { + int error; + string out_name = "culled_mesh.nc"; + string in_name = "mesh.nc"; + string out_path = ""; + string out_file = ""; + string out_fext = ""; + vector mask_names; + vector mask_ops; + + cout << endl << endl; + cout << "************************************************************" << endl; + cout << "MPAS_CELL_CULLER:\n"; + cout << " C++ version\n"; + cout << " Remove cells/edges/vertices from a NetCDF MPAS Mesh file. \n"; + cout << endl << endl; + cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; + cout << "************************************************************" << endl; + cout << endl << endl; + // + // If the input file was not specified, get it now. + // + if ( argc <= 1 ) + { + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Please enter the NetCDF input filename.\n"; + + cin >> in_name; + + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; + + cin >> out_name; + } + else if (argc == 2) + { + in_name = argv[1]; + + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Output name not specified. Using default of culled_mesh.nc\n"; + } + else if (argc == 3) + { + in_name = argv[1]; + out_name = argv[2]; + } + else if (argc >= 10) + { + cout << "\n"; + cout << " ERROR: Incorrect number of arguments specified. See usage statement" << endl; + print_usage(); + exit(1); + } + else + { + cullMasks = true; + in_name = argv[1]; + out_name = argv[2]; + bool foundOperation; + + for ( int i = 3; i < argc; i+=2 ) { + foundOperation = false; + if (strcmp(argv[i], "-m") == 0 ) { + mask_ops.push_back(static_cast(mergeOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-i") == 0 ){ + mask_ops.push_back(static_cast(invertOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-p") == 0 ){ + mask_ops.push_back(static_cast(preserveOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-c") == 0 ){ + outputMap = true; + } else { + cout << " ERROR: Invalid option passed on the command line " << argv[i] << ". Exiting..." << endl; + print_usage(); + exit(1); + } + + if (foundOperation) { + mask_names.push_back( argv[i+1] ); + } + } + } + + if(out_name == in_name){ + cout << " ERROR: Input and Output names are the same." << endl; + return 1; + } + + file_part(out_name, out_path, out_file, out_fext); + + srand(time(NULL)); + + cout << "Reading input grid." << endl; + error = readGridInput(in_name); + if(error) return 1; + + if ( cullMasks ) { + cout << "Reading in mask information." << endl; + for ( int i = 0; i < mask_names.size(); i++ ) { + error = mergeCellMasks(mask_names[i], mask_ops[i]); + if(error) return 1; + } + } + + cout << "Marking cells for removal." << endl; + error = markCells(); + if(error) return 1; + + cout << "Marking vertices for removal." << endl; + error = markVertices(); + if(error) return 1; + + cout << "Marking edges for removal." << endl; + error = markEdges(); + if(error) return 1; + + cout << "Writing grid dimensions" << endl; + if(error = outputGridDimensions(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing grid attributes" << endl; + if(error = outputGridAttributes(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing grid coordinates" << endl; + if(error = mapAndOutputGridCoordinates(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing cell fields and culled_graph.info" << endl; + if(error = mapAndOutputCellFields(in_name, out_path, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing edge fields" << endl; + if(error = mapAndOutputEdgeFields(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing vertex fields" << endl; + if(error = mapAndOutputVertexFields(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Outputting cell map" << endl; + if (outputMap) { + if(error = outputCellMap(out_path)){ + cout << "Error - " << error << endl; + exit(error); + } + } + + return 0; +} + +int outputCellMap(const string outputPath){/*{{{*/ + + int iCell; + ofstream outputfileForward, outputfileBackward; + + // forwards mapping + outputfileForward.open(path_join(outputPath, "cellMapForward.txt")); + + for (iCell=0 ; iCell < nCells ; iCell++) { + + outputfileForward << cellMap.at(iCell) << endl; + + } + + outputfileForward.close(); + + // backwards mapping + int nCellsNew = 0; + vector cellMapBackward; + + cellMapBackward.clear(); + cellMapBackward.resize(nCells); + + for (iCell=0 ; iCell < nCells ; iCell++) { + + if (cellMap.at(iCell) >= 0) { + + cellMapBackward.at(cellMap.at(iCell)) = iCell; + nCellsNew++; + + } + + } + + outputfileBackward.open(path_join(outputPath, "cellMapBackward.txt")); + + for (iCell=0 ; iCell < nCellsNew ; iCell++) { + + outputfileBackward << cellMapBackward.at(iCell) << endl; + + } + + outputfileBackward.close(); + + cellMapBackward.clear(); + + return 0; +}/*}}}*/ + +/* Input/Marking Functions {{{ */ +int readGridInput(const string inputFilename){/*{{{*/ + double *xcell, *ycell,*zcell; + double *xvertex, *yvertex,*zvertex; + int *cellsonvertex_list, *verticesonedge_list, *cellsonedge_list; + string on_a_sphere, is_periodic; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: readGridInput" << endl << endl; +#endif + + ncutil::get_dim(inputFilename, "nCells", nCells); + ncutil::get_dim(inputFilename, "nEdges", nEdges); + ncutil::get_dim(inputFilename, "nVertices", nVertices); + ncutil::get_dim(inputFilename, "vertexDegree", vertexDegree); + ncutil::get_dim(inputFilename, "maxEdges", maxEdges); + + try { +#ifdef _DEBUG + cout << " Reading on_a_sphere" << endl; +#endif + ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); + spherical = (on_a_sphere.find("YES") != string::npos); +#ifdef _DEBUG + cout << " Reading sphere_radius" << endl; +#endif + ncutil::get_att(inputFilename, "sphere_radius", &sphere_radius); +#ifdef _DEBUG + cout << " Reading history" << endl; +#endif + ncutil::get_str(inputFilename, "history", in_history); +#ifdef _DEBUG + cout << " Reading file_id" << endl; +#endif + ncutil::get_str(inputFilename, "file_id", in_file_id); +#ifdef _DEBUG + cout << " Reading parent_id" << endl; +#endif + ncutil::get_str(inputFilename, "parent_id", in_parent_id); +#ifdef _DEBUG + cout << " Reading parent_id" << endl; +#endif + ncutil::get_str(inputFilename, "mesh_spec", in_mesh_spec); +#ifdef _DEBUG + cout << " Reading is_periodic" << endl; +#endif + ncutil::get_str(inputFilename, "is_periodic", is_periodic); + periodic = (is_periodic.find("YES") != string::npos); +#ifdef _DEBUG + cout << " Reading x_period" << endl; +#endif + ncutil::get_att(inputFilename, "x_period", &xPeriod); +#ifdef _DEBUG + cout << " Reading y_period" << endl; +#endif + ncutil::get_att(inputFilename, "y_period", &yPeriod); + + } catch (...) { + // allow errors for optional attr. not found + } + + cout << "Read dimensions:" << endl; + cout << " nCells = " << nCells << endl; + cout << " nVertices = " << nVertices << endl; + cout << " nEdges = " << nEdges << endl; + cout << " vertexDegree = " << vertexDegree << endl; + cout << " maxEdges = " << maxEdges << endl; + cout << " Spherical? = " << spherical << endl; + cout << " Periodic? = " << periodic << endl; + if ( periodic ) { + cout << " x_period = " << xPeriod << endl; + cout << " y_period = " << yPeriod << endl; + } + +#ifdef _DEBUG + cout << " Read areaCell" << endl; +#endif + areaCell.clear(); + areaCell.resize(nCells); + ncutil::get_var(inputFilename, "areaCell", &areaCell[0]); + +#ifdef _DEBUG + cout << " Read cullCell" << endl; +#endif + cullCell.clear(); + cullCell.resize(nCells); + ncutil::get_var(inputFilename, "cullCell", &cullCell[0]); + + // Build cellsOnVertex information + cellsonvertex_list = new int[nVertices * vertexDegree]; + +#ifdef _DEBUG + cout << " Read cellsOnVertex" << endl; +#endif + ncutil::get_var(inputFilename, "cellsOnVertex", cellsonvertex_list); + cellsOnVertex.resize(nVertices); + + for(int i = 0; i < nVertices; i++){ + for(int j = 0; j < vertexDegree; j++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); + } + } + + delete[] cellsonvertex_list; + + // Build verticesOnEdge information + verticesonedge_list = new int[nEdges * 2]; + verticesOnEdge.clear(); + verticesOnEdge.resize(nEdges); + +#ifdef _DEBUG + cout << " Read verticesOnEdge" << endl; +#endif + ncutil::get_var(inputFilename, "verticesOnEdge", verticesonedge_list); + for(int i = 0; i < nEdges; i++){ + // Subtract 1 to convert into base 0 (c index space). + verticesOnEdge.at(i).push_back(verticesonedge_list[i*2] - 1); + verticesOnEdge.at(i).push_back(verticesonedge_list[i*2+1] - 1); + } + + delete[] verticesonedge_list; + + // Build cellsOnEdge information + cellsonedge_list = new int[nEdges * 2]; + cellsOnEdge.clear(); + cellsOnEdge.resize(nEdges); + +#ifdef _DEBUG + cout << " Read cellsOnEdge" << endl; +#endif + ncutil::get_var(inputFilename, "cellsOnEdge", cellsonedge_list); + for(int i = 0; i < nEdges; i++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnEdge.at(i).push_back(cellsonedge_list[i*2] - 1); + cellsOnEdge.at(i).push_back(cellsonedge_list[i*2+1] - 1); + } + + delete[] cellsonedge_list; + + return 0; +}/*}}}*/ +int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ + size_t nRegions = 0, nTransects = 0; + int *regionCellMasks, *transectCellMasks, *cellSeedMask, *flattenedMask; + int i, j; + + try { + ncutil::get_dim(masksFilename, "nRegions", nRegions); + ncutil::get_dim(masksFilename, "nTransects", nTransects); + } catch (...) { + // allow errors for optional attr. not found + } + + regionCellMasks = new int[nCells*nRegions]; + transectCellMasks = new int[nCells*nTransects]; + cellSeedMask = new int[nCells]; + flattenedMask = new int[nCells]; + + try { + ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); + ncutil::get_var(masksFilename, "transectCellMasks", transectCellMasks); + ncutil::get_var(masksFilename, "cellSeedMask", cellSeedMask); + } catch (...) { + // allow errors for optional vars. not found + } + + for ( i = 0; i < nCells; i++){ + flattenedMask[i] = cellSeedMask[i]; + for ( j = 0; j < nRegions; j++){ + flattenedMask[i] = max(flattenedMask[i], regionCellMasks[i * nRegions + j]); + } + + for ( j = 0; j < nTransects; j++ ) { + flattenedMask[i] = max(flattenedMask[i], transectCellMasks[i * nTransects + j]); + } + } + + if ( maskOp == invertOp || maskOp == mergeOp ) { + if ( maskOp == invertOp ) { + for (i = 0; i < nCells; i++){ + flattenedMask[i] = (flattenedMask[i] + 1) % 2; + } + } + + for ( i = 0; i < nCells; i++ ){ + cullCell[i] = max(cullCell[i], flattenedMask[i]); + } + } else if ( maskOp == preserveOp ) { + for ( i = 0; i < nCells; i++ ) { + if ( flattenedMask[i] && cullCell[i] ) { + cullCell[i] = 0; + } + } + } + + delete[] cellSeedMask; + delete[] transectCellMasks; + delete[] regionCellMasks; + delete[] flattenedMask; + + return 0; +}/*}}}*/ +int markCells(){/*{{{*/ + int new_idx; + int cells_removed; + cellMap.clear(); + cellMap.resize(nCells); + + new_idx = 0; + cells_removed = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + // Remove all cells with negative area, and cells that shouldn't be in the grid. + if(areaCell.at(iCell) < 0 || cullCell.at(iCell) == 1){ + cellMap.at(iCell) = -1; + cells_removed++; + } else { + cellMap.at(iCell) = new_idx; + new_idx++; + } + } + + cout << "Removing " << cells_removed << " cells." << endl; + + return 0; +}/*}}}*/ +int markVertices(){/*{{{*/ + bool keep_vertex; + int iCell, new_idx; + int vertices_removed; + + vertexMap.clear(); + vertexMap.resize(nVertices); + + new_idx = 0; + vertices_removed = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + + // Only keep vertices that have at least one cell connected to them + // after cell removal. + keep_vertex = false; + for(int j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + iCell = cellsOnVertex.at(iVertex).at(j); + if(iCell != -1) { + keep_vertex = keep_vertex || (cellMap.at(iCell) != -1); + } + } + + if(keep_vertex){ + vertexMap.at(iVertex) = new_idx; + new_idx++; + } else { + vertexMap.at(iVertex) = -1; + vertices_removed++; + } + } + + cout << "Removing " << vertices_removed << " vertices." << endl; + + return 0; +}/*}}}*/ +int markEdges(){/*{{{*/ + bool keep_edge; + int vertex1, vertex2; + int cell1, cell2; + int new_idx; + int edges_removed; + + edgeMap.clear(); + edgeMap.resize(nEdges); + + new_idx = 0; + edges_removed = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + + // Only keep an edge if it has two vertices + // after vertex removal and at least one cell + // after cell removal. + if(vertex2 != -1){ + keep_edge = (vertexMap.at(vertex1) != -1 && vertexMap.at(vertex2) != -1); + } else { + keep_edge = false; + } + + if(cell2 != -1){ + keep_edge = keep_edge && (cellMap.at(cell1) != -1 || cellMap.at(cell2) != -1); + } else { + keep_edge = keep_edge && (cellMap.at(cell1) != -1); + } + + if(keep_edge){ + edgeMap.at(iEdge) = new_idx; + new_idx++; + } else { + edgeMap.at(iEdge) = -1; + edges_removed++; + } + } + + cout << "Removing " << edges_removed << " edges." << endl; + + return 0; +}/*}}}*/ +/*}}}*/ + +/* Mapping/Output functions {{{*/ +int outputGridDimensions( const string outputFilename ){/*{{{*/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + int grid, retv; + if ((retv = nc_create(outputFilename.c_str(), NC_CLOBBER|NC_NETCDF4, &grid))) + { + std::cout << "Can't create file: " << outputFilename << std::endl; + return retv ; + } + + size_t nCellsNew, nEdgesNew, nVerticesNew; + + nCellsNew = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + nCellsNew += (cellMap.at(iCell) != -1); + } + + nVerticesNew = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + nVerticesNew += (vertexMap.at(iVertex) != -1); + } + + nEdgesNew = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + nEdgesNew += (edgeMap.at(iEdge) != -1); + } + + ncutil::def_dim(outputFilename, "nCells", nCellsNew); + ncutil::def_dim(outputFilename, "nEdges", nEdgesNew); + ncutil::def_dim(outputFilename, "nVertices", nVerticesNew); + ncutil::def_dim(outputFilename, "TWO", 2); + ncutil::def_dim(outputFilename, "vertexDegree", vertexDegree); + ncutil::def_dim(outputFilename, "Time", NC_UNLIMITED); + + return 0; +}/*}}}*/ +int outputGridAttributes( const string inputFilename, const string outputFilename ){/*{{{*/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + string history_str = ""; + string id_str = ""; + string parent_str = ""; + + // write attributes + if(!spherical){ + ncutil::put_str(outputFilename, "on_a_sphere", "NO"); + ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, 0.); + } else { + ncutil::put_str(outputFilename, "on_a_sphere", "YES"); + ncutil::put_att(outputFilename, + "sphere_radius", NC_DOUBLE, sphere_radius); + } + + if(!periodic){ + ncutil::put_str(outputFilename, "is_periodic", "NO"); + } else { + ncutil::put_str(outputFilename, "is_periodic", "YES"); + ncutil::put_att(outputFilename, "x_period", NC_DOUBLE, xPeriod); + ncutil::put_att(outputFilename, "y_period", NC_DOUBLE, yPeriod); + } + + history_str += "MpasCellCuller.x "; + history_str += inputFilename; + history_str += " "; + history_str += outputFilename; + if(in_history != ""){ + history_str += "\n"; + history_str += in_history; + } + + if(in_file_id != "" ){ + parent_str = in_file_id; + if(in_parent_id != ""){ + parent_str += "\n"; + parent_str += in_parent_id; + } + ncutil::put_str(outputFilename, "parent_id", parent_str); + } + + id_str = gen_random(ID_LEN); + + ncutil::put_str(outputFilename, "history", history_str); + ncutil::put_str(outputFilename, "mesh_spec", in_mesh_spec); + ncutil::put_str(outputFilename, "Conventions", "MPAS"); + ncutil::put_str(outputFilename, "source", "MpasCellCuller.x"); + ncutil::put_str(outputFilename, "file_id", id_str); + + return 0; +}/*}}}*/ +int mapAndOutputGridCoordinates( const string inputFilename, const string outputFilename) {/*{{{*/ + /************************************************************************ + * + * This function writes the grid coordinates to the netcdf file named + * outputFilename + * This includes all cell centers, vertices, and edges. + * Both cartesian and lat,lon, as well as all of their indices + * + * **********************************************************************/ + + size_t nCellsNew, nEdgesNew, nVerticesNew; + ncutil::get_dim(outputFilename, "nCells", nCellsNew); + ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); + ncutil::get_dim(outputFilename, "nVertices", nVerticesNew); + + int i, idx_map; + + double *xOld, *yOld, *zOld, *latOld, *lonOld; + double *xNew, *yNew, *zNew, *latNew, *lonNew; + int *idxToNew; + + // Build and write cell coordinate arrays + xOld = new double[nCells]; + yOld = new double[nCells]; + zOld = new double[nCells]; + latOld = new double[nCells]; + lonOld = new double[nCells]; + + xNew = new double[nCellsNew]; + yNew = new double[nCellsNew]; + zNew = new double[nCellsNew]; + latNew = new double[nCellsNew]; + lonNew = new double[nCellsNew]; + idxToNew = new int[nCellsNew]; + + ncutil::get_var(inputFilename, "xCell", xOld); + ncutil::get_var(inputFilename, "yCell", yOld); + ncutil::get_var(inputFilename, "zCell", zOld); + ncutil::get_var(inputFilename, "latCell", latOld); + ncutil::get_var(inputFilename, "lonCell", lonOld); + + idx_map = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + xNew[idx_map] = xOld[iCell]; + yNew[idx_map] = yOld[iCell]; + zNew[idx_map] = zOld[iCell]; + latNew[idx_map] = latOld[iCell]; + lonNew[idx_map] = lonOld[iCell]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latCell", + NC_DOUBLE, "latitudes of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "lonCell", + NC_DOUBLE, "longitudes of cell centres", {"nCells"}); + + ncutil::put_var(outputFilename, "latCell", &latNew[0]); + ncutil::put_var(outputFilename, "lonCell", &lonNew[0]); + + ncutil::def_var(outputFilename, "xCell", + NC_DOUBLE, "x-coordinates of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "yCell", + NC_DOUBLE, "y-coordinates of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "zCell", + NC_DOUBLE, "z-coordinates of cell centres", {"nCells"}); + + ncutil::put_var(outputFilename, "xCell", &xNew[0]); + ncutil::put_var(outputFilename, "yCell", &yNew[0]); + ncutil::put_var(outputFilename, "zCell", &zNew[0]); + + ncutil::def_var(outputFilename, "indexToCellID", + NC_INT, "index to cell ID mapping", {"nCells"}); + + ncutil::put_var(outputFilename, "indexToCellID", &idxToNew[0]); + + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; + + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; + + //Build and write edge coordinate arrays + xOld = new double[nEdges]; + yOld = new double[nEdges]; + zOld = new double[nEdges]; + latOld = new double[nEdges]; + lonOld = new double[nEdges]; + + xNew = new double[nEdgesNew]; + yNew = new double[nEdgesNew]; + zNew = new double[nEdgesNew]; + latNew = new double[nEdgesNew]; + lonNew = new double[nEdgesNew]; + idxToNew = new int[nEdgesNew]; + + ncutil::get_var(inputFilename, "xEdge", xOld); + ncutil::get_var(inputFilename, "yEdge", yOld); + ncutil::get_var(inputFilename, "zEdge", zOld); + ncutil::get_var(inputFilename, "latEdge", latOld); + ncutil::get_var(inputFilename, "lonEdge", lonOld); + + idx_map = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + xNew[idx_map] = xOld[iEdge]; + yNew[idx_map] = yOld[iEdge]; + zNew[idx_map] = zOld[iEdge]; + latNew[idx_map] = latOld[iEdge]; + lonNew[idx_map] = lonOld[iEdge]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latEdge", + NC_DOUBLE, "latitudes of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "lonEdge", + NC_DOUBLE, "longitudes of edge centres", {"nEdges"}); + + ncutil::put_var(outputFilename, "latEdge", &latNew[0]); + ncutil::put_var(outputFilename, "lonEdge", &lonNew[0]); + + ncutil::def_var(outputFilename, "xEdge", + NC_DOUBLE, "x-coordinates of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "yEdge", + NC_DOUBLE, "y-coordinates of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "zEdge", + NC_DOUBLE, "z-coordinates of edge centres", {"nEdges"}); + + ncutil::put_var(outputFilename, "xEdge", &xNew[0]); + ncutil::put_var(outputFilename, "yEdge", &yNew[0]); + ncutil::put_var(outputFilename, "zEdge", &zNew[0]); + + ncutil::def_var(outputFilename, "indexToEdgeID", + NC_INT, "index to edge ID mapping", {"nEdges"}); + + ncutil::put_var(outputFilename, "indexToEdgeID", &idxToNew[0]); + + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; + + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; + + //Build and write vertex coordinate arrays + xOld = new double[nVertices]; + yOld = new double[nVertices]; + zOld = new double[nVertices]; + latOld = new double[nVertices]; + lonOld = new double[nVertices]; + + xNew = new double[nVerticesNew]; + yNew = new double[nVerticesNew]; + zNew = new double[nVerticesNew]; + latNew = new double[nVerticesNew]; + lonNew = new double[nVerticesNew]; + idxToNew = new int[nVerticesNew]; + + ncutil::get_var(inputFilename, "xVertex", xOld); + ncutil::get_var(inputFilename, "yVertex", yOld); + ncutil::get_var(inputFilename, "zVertex", zOld); + ncutil::get_var(inputFilename, "latVertex", latOld); + ncutil::get_var(inputFilename, "lonVertex", lonOld); + + idx_map = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + if(vertexMap.at(iVertex) != -1){ + xNew[idx_map] = xOld[iVertex]; + yNew[idx_map] = yOld[iVertex]; + zNew[idx_map] = zOld[iVertex]; + latNew[idx_map] = latOld[iVertex]; + lonNew[idx_map] = lonOld[iVertex]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latVertex", + NC_DOUBLE, "latitudes of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "lonVertex", + NC_DOUBLE, "longitudes of vertices", {"nVertices"}); + + ncutil::put_var(outputFilename, "latVertex", &latNew[0]); + ncutil::put_var(outputFilename, "lonVertex", &lonNew[0]); + + ncutil::def_var(outputFilename, "xVertex", + NC_DOUBLE, "x-coordinates of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "yVertex", + NC_DOUBLE, "y-coordinates of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "zVertex", + NC_DOUBLE, "z-coordinates of vertices", {"nVertices"}); + + ncutil::put_var(outputFilename, "xVertex", &xNew[0]); + ncutil::put_var(outputFilename, "yVertex", &yNew[0]); + ncutil::put_var(outputFilename, "zVertex", &zNew[0]); + + ncutil::def_var(outputFilename, "indexToVertexID", + NC_INT, "index to vertex ID mapping", {"nVertices"}); + + ncutil::put_var(outputFilename, "indexToVertexID", &idxToNew[0]); + + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; + + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; + + return 0; +}/*}}}*/ +int mapAndOutputCellFields( const string inputFilename, const string outputPath, + const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function maps and writes all of the cell related fields. Including + * cellsOnCell + * edgesOnCell + * verticesOnCell + * nEdgesonCell + * areaCell + * meshDensity + * + * It also writes the graph.info file which can be used to decompose the mesh. + * + * ***************************************************************/ + + size_t nCellsNew, nEdgesNew, maxEdgesNew, edgeCount; + ncutil::get_dim(outputFilename, "nCells", nCellsNew); + ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); + + double *meshDensityOld, *meshDensityNew; + double *areaCellNew; + int *tmp_arr_old, *nEdgesOnCellOld, *nEdgesOnCellNew; + int *tmp_arr_new; + + tmp_arr_old = new int[nCells*maxEdges]; + nEdgesOnCellOld = new int[nCells]; + nEdgesOnCellNew = new int[nCellsNew]; + + ncutil::get_var(inputFilename, "edgesOnCell", tmp_arr_old); + ncutil::get_var(inputFilename, "nEdgesOnCell", nEdgesOnCellOld); + + // Need to map nEdgesOnCell to get maxEdges + maxEdgesNew = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + nEdgesOnCellNew[cellMap.at(iCell)] = nEdgesOnCellOld[iCell]; + maxEdgesNew = max(maxEdgesNew, (size_t)nEdgesOnCellNew[cellMap.at(iCell)]); + } + } + tmp_arr_new = new int[nCells * maxEdgesNew]; + + // Write maxEdges and maxEdges2 to output file + ncutil::def_dim(outputFilename, "maxEdges", maxEdgesNew); + ncutil::def_dim(outputFilename, "maxEdges2", maxEdgesNew * 2); + + // Write nEdgesOncell to output file + ncutil::def_var(outputFilename, "nEdgesOnCell", + NC_INT, "number of edges on each cell", {"nCells"}); + + ncutil::put_var(outputFilename, "nEdgesOnCell", &nEdgesOnCellNew[0]); + + // Map edgesOnCell + for(int iCell = 0; iCell < nCells; iCell++){ +#ifdef _DEBUG + cout << "On cell: " << iCell << endl; +#endif + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < maxEdgesNew; j++){ + int iEdge = tmp_arr_old[iCell*maxEdges + j] - 1; + + if(iEdge != -1 && iEdge < edgeMap.size()){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = edgeMap.at(iEdge)+1; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } + +#ifdef _DEBUG + cout << " Mapping edge: " << iEdge << " to " << + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] << " dbg info: " << + tmp_arr_old[iCell*maxEdges + j] << " " << j << endl; +#endif + } + } + } + + ncutil::def_var(outputFilename, "edgesOnCell", + NC_INT, "edges on each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "edgesOnCell", &tmp_arr_new[0]); + + ncutil::get_var(inputFilename, "cellsOnCell", tmp_arr_old); + + // Map cellsOnCell, and determine number of edges in graph. + edgeCount = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < nEdgesOnCellOld[iCell]; j++){ + int coc = tmp_arr_old[iCell*maxEdges + j] - 1; + + if(coc != -1 && coc < nCells && cellMap.at(coc) < nCellsNew && cellMap.at(coc) != -1){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = cellMap.at(coc)+1; + edgeCount++; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } + } + } + } + edgeCount = edgeCount / 2; + + // Build graph.info file + ofstream graph(path_join(outputPath, "culled_graph.info")); + graph << nCellsNew << " " << edgeCount << endl; + for(int iCell = 0; iCell < nCellsNew; iCell++){ + for(int j = 0; j < nEdgesOnCellNew[iCell]; j++){ + if (tmp_arr_new[iCell * maxEdgesNew + j] != 0) { + graph << tmp_arr_new[iCell * maxEdgesNew + j] << " "; + } + } + graph << endl; + } + graph.close(); + + ncutil::def_var(outputFilename, "cellsOnCell", + NC_INT, "cells adj. to each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "cellsOnCell", &tmp_arr_new[0]); + + delete[] nEdgesOnCellNew; + delete[] nEdgesOnCellOld; + + ncutil::get_var(inputFilename, "verticesOnCell", tmp_arr_old); + + // Map verticesOnCell + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < maxEdgesNew; j++){ + int iVertex = tmp_arr_old[iCell*maxEdges + j] - 1; + + if(iVertex != -1 && iVertex < vertexMap.size()){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = vertexMap.at(iVertex)+1; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } + } + } + } + + ncutil::def_var(outputFilename, "verticesOnCell", + NC_INT, "vertices on each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "verticesOnCell", &tmp_arr_new[0]); + + delete[] tmp_arr_old; + delete[] tmp_arr_new; + + // Map areaCell + areaCellNew = new double[nCellsNew]; + + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + areaCellNew[cellMap.at(iCell)] = areaCell.at(iCell); + } + } + + ncutil::def_var(outputFilename, "areaCell", + NC_DOUBLE, "surface area of each cell", {"nCells"}); + + ncutil::put_var(outputFilename, "areaCell", &areaCellNew[0]); + + delete[] areaCellNew; + + // Map meshDensity + meshDensityOld = new double[nCells]; + meshDensityNew = new double[nCellsNew]; + + ncutil::get_var(inputFilename, "meshDensity", meshDensityOld); + + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + meshDensityNew[cellMap.at(iCell)] = meshDensityOld[iCell]; + } + } + + ncutil::def_var(outputFilename, "meshDensity", + NC_DOUBLE, "mesh density distribution", {"nCells"}); + + ncutil::put_var(outputFilename, "meshDensity", &meshDensityNew[0]); + + return 0; +}/*}}}*/ +int mapAndOutputEdgeFields( const string inputFilename, const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function maps and writes all of the edge related fields. Including + * cellsOnEdge + * edgesOnEdge + * verticesOnEdge + * nEdgesOnEdge + * weightsOnEdge + * dvEdge + * dcEdge + * angleEdge + * + * ***************************************************************/ + + size_t nEdgesNew, maxEdges2New, two = 2; + ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); + ncutil::get_dim(outputFilename, "maxEdges2", maxEdges2New); + + int *nEdgesOnEdgeOld, *nEdgesOnEdgeNew; + int *edgesOnEdgeOld, *edgesOnEdgeNew; + int *cellsOnEdgeOld, *cellsOnEdgeNew; + int *verticesOnEdgeOld, *verticesOnEdgeNew; + double *weightsOnEdgeOld, *weightsOnEdgeNew; + double *dvEdgeOld, *dvEdgeNew; + double *dcEdgeOld, *dcEdgeNew; + double *angleEdgeOld, *angleEdgeNew; + + // Need to map cellsOnEdge and verticesOnEdge + cellsOnEdgeOld = new int[nEdges*2]; + cellsOnEdgeNew = new int[nEdgesNew*2]; + verticesOnEdgeOld = new int[nEdges*2]; + verticesOnEdgeNew = new int[nEdgesNew*2]; + + ncutil::get_var(inputFilename, "cellsOnEdge", cellsOnEdgeOld); + ncutil::get_var(inputFilename, "verticesOnEdge", verticesOnEdgeOld); + + // Map cellsOnEdge and verticesOnEdge + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + int cell1, cell2; + int vertex1, vertex2; + + cell1 = cellsOnEdgeOld[iEdge * 2] - 1; + cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; + vertex1 = verticesOnEdgeOld[iEdge * 2] - 1; + vertex2 = verticesOnEdgeOld[iEdge * 2 + 1] - 1; + +#ifdef _DEBUG + cout << "Defining edge: " << endl; + cout << " Old cell1: " << cell1 << endl; + cout << " Old cell2: " << cell2 << endl; + cout << " Old vertex1: " << vertex1 << endl; + cout << " Old vertex2: " << vertex2 << endl; +#endif + + if(cell1 != -1 && cell2 != -1){ + if(cellMap.at(cell1) != -1 && cellMap.at(cell2) != -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = cellMap.at(cell2) + 1; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else if (cellMap.at(cell2) == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else if (cellMap.at(cell1) == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; + } + } else if(cell2 == -1){ + if(cellMap.at(cell1) != -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else { + cout << "ERROR: Edge mask is 1, but has no cells." << endl; + } + } else if(cell1 == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; + } else { + cout << "ERROR: Edge mask is 1, but has no cells." << endl; + } + +#ifdef _DEBUG + cout << " New cell1: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2] << endl; + cout << " New cell2: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; + cout << " New vertex1: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2] << endl; + cout << " New vertex2: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; +#endif + } + } + + ncutil::def_var(outputFilename, "verticesOnEdge", + NC_INT, "vertices on each edge", {"nEdges", "TWO"}); + ncutil::def_var(outputFilename, "cellsOnEdge", + NC_INT, "cells adj. to each edge", {"nEdges", "TWO"}); + + ncutil::put_var(outputFilename, "verticesOnEdge", &verticesOnEdgeNew[0]); + ncutil::put_var(outputFilename, "cellsOnEdge", &cellsOnEdgeNew[0]); + + // Don't delete cellsOnEdgeOld yet. It's needed to map edgesOnEdge and weightsOnEdge + delete[] cellsOnEdgeNew; + delete[] verticesOnEdgeOld; + delete[] verticesOnEdgeNew; + + // Map edgesOnEdge, nEdgesOnEdge, and weightsOnEdge + nEdgesOnEdgeOld = new int[nEdges]; + nEdgesOnEdgeNew = new int[nEdges]; + edgesOnEdgeOld = new int[nEdges*maxEdges*2]; + edgesOnEdgeNew = new int[nEdgesNew*maxEdges2New]; + weightsOnEdgeOld = new double[nEdges*maxEdges*2]; + weightsOnEdgeNew = new double[nEdgesNew*maxEdges2New]; + + ncutil::get_var(inputFilename, "nEdgesOnEdge", nEdgesOnEdgeOld); + ncutil::get_var(inputFilename, "edgesOnEdge", edgesOnEdgeOld); + ncutil::get_var(inputFilename, "weightsOnEdge", weightsOnEdgeOld); + + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + int edgeCount = 0; + if(edgeMap.at(iEdge) != -1){ + int cell1, cell2; + + cell1 = cellsOnEdgeOld[iEdge * 2] - 1; + cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; + + if(cell1 != -1){ + cell1 == cellMap.at(cell1); + } + + if(cell2 != -1){ + cell2 == cellMap.at(cell1); + } + + if(cell1 != -1 && cell2 != -1){ + for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ + int eoe = edgesOnEdgeOld[iEdge*maxEdges*2 + j] - 1; + + if(eoe != -1 && eoe < edgeMap.size()){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = edgeMap.at(eoe) + 1; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = + weightsOnEdgeOld[iEdge*maxEdges*2 + j]; + edgeCount++; + } else { + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + } + } else if ( cell1 == -1 || cell2 == -1){ + for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + } + + for(int j = edgeCount; j < maxEdges2New; j++){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + nEdgesOnEdgeNew[edgeMap.at(iEdge)] = edgeCount; + } + } + + ncutil::def_var(outputFilename, "nEdgesOnEdge", + NC_INT, "number of edges adj. to each edge", {"nEdges"}); + ncutil::def_var(outputFilename, "edgesOnEdge", + NC_INT, "edges adj. to each edge", {"nEdges", "maxEdges2"}); + + ncutil::put_var(outputFilename, "nEdgesOnEdge", &nEdgesOnEdgeNew[0]); + ncutil::put_var(outputFilename, "edgesOnEdge", &edgesOnEdgeNew[0]); + + ncutil::def_var(outputFilename, "weightsOnEdge", + NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); + + ncutil::put_var(outputFilename, "weightsOnEdge", &weightsOnEdgeNew[0]); + + delete[] nEdgesOnEdgeOld; + delete[] cellsOnEdgeOld; + delete[] edgesOnEdgeOld; + delete[] edgesOnEdgeNew; + delete[] weightsOnEdgeOld; + delete[] weightsOnEdgeNew; + + // Map dvEdge, dcEdge, and angleEdge + dvEdgeOld = new double[nEdges]; + dcEdgeOld = new double[nEdges]; + angleEdgeOld = new double[nEdges]; + dvEdgeNew = new double[nEdgesNew]; + dcEdgeNew = new double[nEdgesNew]; + angleEdgeNew = new double[nEdgesNew]; + + ncutil::get_var(inputFilename, "dvEdge", dvEdgeOld); + ncutil::get_var(inputFilename, "dcEdge", dcEdgeOld); + ncutil::get_var(inputFilename, "angleEdge", angleEdgeOld); + + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + dvEdgeNew[edgeMap.at(iEdge)] = dvEdgeOld[iEdge]; + dcEdgeNew[edgeMap.at(iEdge)] = dcEdgeOld[iEdge]; + angleEdgeNew[edgeMap.at(iEdge)] = angleEdgeOld[iEdge]; + } + } + + ncutil::def_var(outputFilename, "dvEdge", + NC_DOUBLE, "length of arc between centres", {"nEdges"}); + ncutil::def_var(outputFilename, "dcEdge", + NC_DOUBLE, "length of arc between centres", {"nEdges"}); + ncutil::def_var(outputFilename, "angleEdge", + NC_DOUBLE, "angle to edges", {"nEdges"}) ; + + ncutil::put_var(outputFilename, "dvEdge", &dvEdgeNew[0]); + ncutil::put_var(outputFilename, "dcEdge", &dcEdgeNew[0]); + ncutil::put_var(outputFilename, "angleEdge", &angleEdgeNew[0]); + + delete[] dvEdgeOld; + delete[] dvEdgeNew; + delete[] dcEdgeOld; + delete[] dcEdgeNew; + delete[] angleEdgeOld; + delete[] angleEdgeNew; + + return 0; +}/*}}}*/ +int mapAndOutputVertexFields( const string inputFilename, const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function maps and writes all of the vertex related fields. Including + * cellsOnVertex + * edgesOnVertex + * areaTriangle + * kiteAreasOnVertex + * + * ***************************************************************/ + + size_t nVerticesNew; + ncutil::get_dim(outputFilename, "nVertices", nVerticesNew); + + int *cellsOnVertexOld, *cellsOnVertexNew; + int *edgesOnVertexOld, *edgesOnVertexNew; + double *kiteAreasOnVertexOld, *kiteAreasOnVertexNew; + double *areaTriangleOld, *areaTriangleNew; + + cellsOnVertexOld = new int[nVertices * vertexDegree]; + cellsOnVertexNew = new int[nVerticesNew * vertexDegree]; + edgesOnVertexOld = new int[nVertices * vertexDegree]; + edgesOnVertexNew = new int[nVerticesNew * vertexDegree]; + kiteAreasOnVertexOld = new double[nVertices * vertexDegree]; + kiteAreasOnVertexNew = new double[nVerticesNew * vertexDegree]; + areaTriangleOld = new double[nVertices]; + areaTriangleNew = new double[nVerticesNew]; + + ncutil::get_var(inputFilename, "cellsOnVertex", cellsOnVertexOld); + ncutil::get_var(inputFilename, "edgesOnVertex", edgesOnVertexOld); + ncutil::get_var(inputFilename, "kiteAreasOnVertex", kiteAreasOnVertexOld); + ncutil::get_var(inputFilename, "areaTriangle", areaTriangleOld); + + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + double area = 0.0; + if(vertexMap.at(iVertex) != -1){ + for(int j = 0; j < vertexDegree; j++){ + int iCell, iEdge; + + iCell = cellsOnVertexOld[iVertex*vertexDegree + j] - 1; + iEdge = edgesOnVertexOld[iVertex*vertexDegree + j] - 1; + + if(iCell != -1){ + cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = cellMap.at(iCell) + 1; + if(cellMap.at(iCell) == -1){ + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; + } else { + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = + kiteAreasOnVertexOld[iVertex*vertexDegree + j]; + } + area += kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j]; + } else { + cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; + } + + if(iEdge != -1){ + edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = edgeMap.at(iEdge) + 1; + } else { + edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; + } + } + + areaTriangleNew[vertexMap.at(iVertex)] = area; + } + } + + ncutil::def_var(outputFilename, "edgesOnVertex", + NC_INT, "edges adj. to each vertex", {"nVertices", "vertexDegree"}); + ncutil::def_var(outputFilename, "cellsOnVertex", + NC_INT, "cells adj. to each vertex", {"nVertices", "vertexDegree"}); + + ncutil::put_var(outputFilename, "edgesOnVertex", &edgesOnVertexNew [0]); + ncutil::put_var(outputFilename, "cellsOnVertex", &cellsOnVertexNew [0]); + + ncutil::def_var(outputFilename, "areaTriangle", + NC_DOUBLE, "surface area of dual cells", {"nVertices"}); + ncutil::def_var(outputFilename, "kiteAreasOnVertex", + NC_DOUBLE, + "surface areas of overlap between cells and dual cells", {"nVertices", "vertexDegree"}); + + ncutil::put_var(outputFilename, "areaTriangle", &areaTriangleNew [0]); + ncutil::put_var(outputFilename, "kiteAreasOnVertex", &kiteAreasOnVertexNew [0]); + + delete[] cellsOnVertexOld; + delete[] cellsOnVertexNew; + delete[] edgesOnVertexOld; + delete[] edgesOnVertexNew; + delete[] kiteAreasOnVertexOld; + delete[] kiteAreasOnVertexNew; + delete[] areaTriangleOld; + delete[] areaTriangleNew; + + return 0; +}/*}}}*/ +/*}}}*/ + +string gen_random(const int len) {/*{{{*/ + static const char alphanum[] = + "0123456789" +// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + string rand_str = ""; + + for (int i = 0; i < len; ++i) { + rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; + } + + return rand_str; +}/*}}}*/ + diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp new file mode 100755 index 000000000..ac22939ae --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp @@ -0,0 +1,3231 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netcdf_utils.h" + +#include "pnt.h" +#include "edge.h" + +#include "string_utils.h" + +#define MESH_SPEC 1.0 +#define ID_LEN 10 + +using namespace std; + +int nCells, nVertices, vertex_degree; +int maxEdges; +int obtuseTriangles = 0; +bool spherical, periodic; +double sphereRadius, xPeriod, yPeriod; +double xCellRange[2]; +double yCellRange[2]; +double zCellRange[2]; +double xVertexRange[2]; +double yVertexRange[2]; +double zVertexRange[2]; +double xCellDistance, yCellDistance, zCellDistance; +double xVertexDistance, yVertexDistance, zVertexDistance; +double xPeriodicFix, yPeriodicFix; +string in_history = ""; +string in_file_id = ""; +string in_parent_id = ""; + +// Connectivity and location information {{{ + +vector cells; +vector edges; +vector vertices; +vector completeCellMask; +vector nEdgesOnCell; +vector< vector > cellsOnEdge; +vector< vector > verticesOnEdge; +vector< vector > edgesOnVertex; +vector< vector > cellsOnVertex; +vector< vector > cellsOnCell; +vector< vector > edgesOnCell; +vector< vector > verticesOnCell; +vector< vector > edgesOnEdge; +vector< vector > weightsOnEdge; +vector< vector > kiteAreasOnVertex; +vector dvEdge; +vector dcEdge; +vector areaCell; +vector areaTriangle; +vector angleEdge; +vector meshDensity; +vector cellQuality; +vector gridSpacing; +vector triangleQuality; +vector triangleAngleQuality; +vector obtuseTriangle; + +// }}} + +// Iterators {{{ +vector::iterator pnt_itr; +vector::iterator int_itr; +vector< vector >::iterator vec_int_itr; +vector< vector >::iterator vec_dbl_itr; +vector::iterator dbl_itr; +// }}} +struct int_hasher {/*{{{*/ + size_t operator()(const int i) const { + return (size_t)i; + } +};/*}}}*/ + +/* Building/Ordering functions {{{ */ +int readGridInput(const string inputFilename); +int buildUnorderedCellConnectivity(); +int firstOrderingVerticesOnCell(); +int buildCompleteCellMask(); +int buildEdges(); +int orderVertexArrays(); +int orderCellArrays(); +int buildAreas(); +int buildEdgesOnEdgeArrays(); +int buildAngleEdge(); +int buildMeshQualities(); +/*}}}*/ + +/* Output functions {{{*/ +int outputGridDimensions(const string outputFilename); +int outputGridAttributes(const string outputFilename, const string inputFilename); +int outputGridCoordinates(const string outputFilename); +int outputCellConnectivity(const string outputFilename); +int outputEdgeConnectivity(const string outputFilename); +int outputVertexConnectivity(const string outputFilename); +int outputCellParameters(const string outputFilename); +int outputVertexParameters(const string outputFilename); +int outputEdgeParameters(const string outputFilename); +int outputMeshDensity(const string outputFilename); +int outputMeshQualities(const string outputFilename); +int writeGraphFile(const string outputFilename); +/*}}}*/ + +string gen_random(const int len); + +int main ( int argc, char *argv[] ) { + int error; + string out_name = "mesh.nc"; + string in_name = "grid.nc"; + string out_path = ""; + string out_file = ""; + string out_fext = ""; + + cout << endl << endl; + cout << "************************************************************" << endl; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " C++ version\n"; + cout << " Convert a NetCDF file describing Cell Locations, \n"; + cout << " Vertex Location, and Connectivity into a valid MPAS mesh.\n"; + cout << endl << endl; + cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; + cout << "************************************************************" << endl; + cout << endl << endl; + // + // If the input file was not specified, get it now. + // + if ( argc <= 1 ) + { + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Please enter the NetCDF input filename.\n"; + + cin >> in_name; + + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; + + cin >> out_name; + } + else if (argc == 2) + { + in_name = argv[1]; + + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Output name not specified. Using default of mesh.nc\n"; + } + else if (argc == 3) + { + in_name = argv[1]; + out_name = argv[2]; + } + + if(in_name == out_name){ + cout << " ERROR: Input and output names are the same." << endl; + return 1; + } + + file_part(out_name, out_path, out_file, out_fext); + + srand(time(NULL)); + + cout << "Reading input grid." << endl; + error = readGridInput(in_name); + if(error) return 1; + + cout << "Build prelimiary cell connectivity." << endl; + error = buildUnorderedCellConnectivity(); + if(error) return 1; + + cout << "Order vertices on cell." << endl; + error = firstOrderingVerticesOnCell(); + if(error) return 1; + + cout << "Build complete cell mask." << endl; + error = buildCompleteCellMask(); + if(error) return 1; + + cout << "Build and order edges, dvEdge, and dcEdge." << endl; + error = buildEdges(); + if(error) return 1; + + cout << "Build and order vertex arrays," << endl; + error = orderVertexArrays(); + if(error) return 1; + + cout << "Build and order cell arrays," << endl; + error = orderCellArrays(); + if(error) return 1; + + cout << "Build areaCell, areaTriangle, and kiteAreasOnVertex." << endl; + error = buildAreas(); + if(error) return 1; + + cout << "Build edgesOnEdge and weightsOnEdge." << endl; + error = buildEdgesOnEdgeArrays(); + if(error) return 1; + + cout << "Build angleEdge." << endl; + error = buildAngleEdge(); + if(error) return 1; + + cout << "Building mesh qualities." << endl; + error = buildMeshQualities(); + if(error) return 1; + + cout << "Writing grid dimensions" << endl; + if(error = outputGridDimensions(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing grid attributes" << endl; + if(error = outputGridAttributes(out_name, in_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing grid coordinates" << endl; + if(error = outputGridCoordinates(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing cell connectivity" << endl; + if(error = outputCellConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing edge connectivity" << endl; + if(error = outputEdgeConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing vertex connectivity" << endl; + if(error = outputVertexConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing cell parameters" << endl; + if(error = outputCellParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing edge parameters" << endl; + if(error = outputEdgeParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing vertex parameters" << endl; + if(error = outputVertexParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing mesh qualities" << endl; + if(error = outputMeshQualities(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Reading and writing meshDensity" << endl; + if(error = outputMeshDensity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Write graph.info file" << endl; + if(error = writeGraphFile(path_join(out_path, "graph.info"))){ + cout << "Error - " << error << endl; + exit(error); + } +} + +/* Building/Ordering functions {{{ */ +int readGridInput(const string inputFilename){/*{{{*/ + int ncid, retv; + size_t nCells, nVertices, vertexDegree; + string on_a_sphere, is_periodic; + double *xcell, *ycell,*zcell; + double *xvertex, *yvertex,*zvertex; + int *cellsonvertex_list; + pnt new_location; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: readGridInput" << endl << endl; +#endif + + // Initialize range mins with huge values. + xCellRange[0] = 1E10; + yCellRange[0] = 1E10; + zCellRange[0] = 1E10; + xVertexRange[0] = 1E10; + yVertexRange[0] = 1E10; + zVertexRange[0] = 1E10; + + // Initialize range maxes with small values. + xCellRange[1] = -1E10; + yCellRange[1] = -1E10; + zCellRange[1] = -1E10; + xVertexRange[1] = -1E10; + yVertexRange[1] = -1E10; + zVertexRange[1] = -1E10; + + ncutil::get_dim(inputFilename, "nCells", nCells); + ncutil::get_dim(inputFilename, "nVertices", nVertices); + ncutil::get_dim(inputFilename, "vertexDegree", vertexDegree); + + vertex_degree = vertexDegree; + + try { +#ifdef _DEBUG + cout << " Reading on_a_sphere" << endl; +#endif + ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); + spherical = (on_a_sphere.find("YES") != string::npos); +#ifdef _DEBUG + cout << " Reading sphere_radius" << endl; +#endif + ncutil::get_att(inputFilename, "sphere_radius", &sphereRadius); +#ifdef _DEBUG + cout << " Reading history" << endl; +#endif + ncutil::get_str(inputFilename, "history", in_history); +#ifdef _DEBUG + cout << " Reading file_id" << endl; +#endif + ncutil::get_str(inputFilename, "file_id", in_file_id); +#ifdef _DEBUG + cout << " Reading parent_id" << endl; +#endif + ncutil::get_str(inputFilename, "parent_id", in_parent_id); +#ifdef _DEBUG + cout << " Reading is_periodic" << endl; +#endif + ncutil::get_str(inputFilename, "is_periodic", is_periodic); + periodic = (is_periodic.find("YES") != string::npos); +#ifdef _DEBUG + cout << " Reading x_period" << endl; +#endif + ncutil::get_att(inputFilename, "x_period", &xPeriod); +#ifdef _DEBUG + cout << " Reading y_period" << endl; +#endif + ncutil::get_att(inputFilename, "y_period", &yPeriod); + + } catch (...) { + // allow errors for optional attr. not found + } + + cout << "Read dimensions:" << endl; + cout << " nCells = " << nCells << endl; + cout << " nVertices = " << nVertices << endl; + cout << " vertexDegree = " << vertexDegree << endl; + cout << " Spherical? = " << spherical << endl; + cout << " Periodic? = " << periodic << endl; + if ( periodic ) { + cout << " x_period = " << xPeriod << endl; + cout << " y_period = " << yPeriod << endl; + } + + // Build cell center location information + xcell = new double[nCells]; + ycell = new double[nCells]; + zcell = new double[nCells]; + + ncutil::get_var(inputFilename, "xCell", xcell); + ncutil::get_var(inputFilename, "yCell", ycell); + ncutil::get_var(inputFilename, "zCell", zcell); + + cells.clear(); + for(int i = 0; i < nCells; i++){ + xCellRange[0] = min(xCellRange[0], xcell[i]); + xCellRange[1] = max(xCellRange[1], xcell[i]); + + yCellRange[0] = min(yCellRange[0], ycell[i]); + yCellRange[1] = max(yCellRange[1], ycell[i]); + + zCellRange[0] = min(zCellRange[0], zcell[i]); + zCellRange[1] = max(zCellRange[1], zcell[i]); + + new_location = pnt(xcell[i], ycell[i], zcell[i], i); + + if(spherical) new_location.normalize(); + cells.push_back(new_location); + } + + cout << "Built " << cells.size() << " cells." << endl; + delete[] xcell; + delete[] ycell; + delete[] zcell; + + // Build vertex location information + xvertex = new double[nVertices]; + yvertex = new double[nVertices]; + zvertex = new double[nVertices]; + + ncutil::get_var(inputFilename, "xVertex", xvertex); + ncutil::get_var(inputFilename, "yVertex", yvertex); + ncutil::get_var(inputFilename, "zVertex", zvertex); + + vertices.clear(); + for(int i = 0; i < nVertices; i++){ + xVertexRange[0] = min(xVertexRange[0], xvertex[i]); + xVertexRange[1] = max(xVertexRange[1], xvertex[i]); + + yVertexRange[0] = min(yVertexRange[0], yvertex[i]); + yVertexRange[1] = max(yVertexRange[1], yvertex[i]); + + zVertexRange[0] = min(zVertexRange[0], zvertex[i]); + zVertexRange[1] = max(zVertexRange[1], zvertex[i]); + + new_location = pnt(xvertex[i], yvertex[i], zvertex[i], i); + if(spherical) new_location.normalize(); + vertices.push_back(new_location); + } + + cout << "Built " << vertices.size() << " vertices." << endl; + delete[] xvertex; + delete[] yvertex; + delete[] zvertex; + + // Build unordered cellsOnVertex information + cellsonvertex_list = new int[nVertices * vertexDegree]; + + ncutil::get_var(inputFilename, "cellsOnVertex", cellsonvertex_list); + cellsOnVertex.resize(nVertices); + + for(int i = 0; i < nVertices; i++){ + for(int j = 0; j < vertexDegree; j++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); + } + } + + delete[] cellsonvertex_list; + + meshDensity.clear(); + meshDensity.resize(cells.size()); + ncutil::get_var(inputFilename, "meshDensity", &meshDensity[0]); + + xCellDistance = fabs(xCellRange[1] - xCellRange[0]); + yCellDistance = fabs(yCellRange[1] - yCellRange[0]); + zCellDistance = fabs(zCellRange[1] - zCellRange[0]); + + xVertexDistance = fabs(xVertexRange[1] - xVertexRange[0]); + yVertexDistance = fabs(yVertexRange[1] - yVertexRange[0]); + zVertexDistance = fabs(zVertexRange[1] - zVertexRange[0]); + + if(periodic){ + // Quads are not staggered in the y direction, so need to period by + // max distance + min distance + if(vertexDegree == 4){ + if(xPeriod < 0.0){ + xPeriodicFix = xCellRange[0] + xCellRange[1]; + } else { + xPeriodicFix = xPeriod; + } + + if(yPeriod < 0.0){ + yPeriodicFix = yCellRange[0] + yCellRange[1]; + } else { + yPeriodicFix = yPeriod; + } + // Triangles can be staggered, so only period my max distance + } else { + if(xPeriod < 0.0){ + xPeriodicFix = xCellRange[1]; + } else { + xPeriodicFix = xPeriod; + } + + if(yPeriod < 0.0){ + yPeriodicFix = yCellRange[1]; + } else { + yPeriodicFix = yPeriod; + } + } + } else { + xPeriodicFix = 1.0e5 * abs(xCellRange[1] + xCellRange[0]); + yPeriodicFix = 1.0e5 * abs(yCellRange[1] + yCellRange[0]); + } + +#ifdef _DEBUG + xPeriod = xPeriodicFix; + yPeriod = yPeriodicFix; + cout << "cell Mins: " << xCellRange[0] << " " << yCellRange[0] << " " << zCellRange[0] << endl; + cout << "vertex Mins: " << xVertexRange[0] << " " << yVertexRange[0] << " " << zVertexRange[0] << endl; + cout << "cell Maxes: " << xCellRange[1] << " " << yCellRange[1] << " " << zCellRange[1] << endl; + cout << "vertex Maxes: " << xVertexRange[1] << " " << yVertexRange[1] << " " << zVertexRange[1] << endl; + cout << "cell Distances: " << xCellDistance << " " << yCellDistance << " " << zCellDistance << endl; + cout << "vertex Distances: " << xVertexDistance << " " << yVertexDistance << " " << zVertexDistance << endl; + cout << "xPeriodicFix: " << xPeriodicFix << endl; + cout << "yPeriodicFix: " << yPeriodicFix << endl; + cout << "xPeriod: " << xPeriod << endl; + cout << "yPeriod: " << yPeriod << endl; +#endif + + if(!spherical && (zCellDistance > 0.0 || zVertexDistance > 0.0)){ + cout << "ERROR:" << endl; + cout << " This point set is defined in the plane, but has non-zero Z coordinates." << endl; + cout << endl; + return 1; + } + + return 0; +}/*}}}*/ +int buildUnorderedCellConnectivity(){/*{{{*/ + // buildUnorderedCellConnectivity should assume that cellsOnVertex hasn't been ordered properly yet. + // It should compute the inverse of cellsOnVertex (verticesOnCell) unordered. + // It will also compute an unordered cellsOnCell that can be considered invalid for actual use (e.g. quad grids). + // This ordering should happen regardless of it the mesh is planar or spherical. + + int iVertex, iCell, newCell, j, k, l, m, matches; + bool add; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildUnorderedCellConnectivity" << endl << endl; +#endif + + verticesOnCell.clear(); + verticesOnCell.resize(cells.size()); + + for(iVertex = 0; iVertex < vertices.size(); iVertex++){ + for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + iCell = cellsOnVertex.at(iVertex).at(j); + if(iCell != -1){ + add = true; + for(int k = 0; k < verticesOnCell.at(iCell).size(); k++){ + if(verticesOnCell.at(iCell).at(k) == iVertex){ + add = false; + } + } + + if(add) { + verticesOnCell.at(iCell).push_back(iVertex); + } + } + } + } + + + cellsOnCell.clear(); + cellsOnCell.resize(cells.size()); + for(iCell = 0; iCell < cells.size(); iCell++){ + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + iVertex = verticesOnCell.at(iCell).at(j); + for(k = 0; k < cellsOnVertex.at(iVertex).size(); k++){ + newCell = cellsOnVertex.at(iVertex).at(k); + + if(newCell != iCell){ + add = true; + for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ + if(cellsOnCell.at(iCell).at(l) == newCell){ + add = false; + } + } + + if(add) { + if(newCell != -1){ + matches = 0; + for(l = 0; l < verticesOnCell.at(iCell).size(); l++){ + for(m = 0; m < verticesOnCell.at(newCell).size(); m++){ + if(verticesOnCell.at(iCell).at(l) == verticesOnCell.at(newCell).at(m)){ + matches++; + } + } + } + + if(matches == 2){ + cellsOnCell.at(iCell).push_back(newCell); +#ifdef _DEBUG + cout << " Found two shared vertices for cell edge. Adding cell." << endl; +#endif + } else { +#ifdef _DEBUG + cout << " Only found " << matches << + " shared vertices for cell edge. Not adding cell." << endl; +#endif + + } + } else { + cellsOnCell.at(iCell).push_back(newCell); + } + } + } + } + } + } + + return 0; +}/*}}}*/ +int firstOrderingVerticesOnCell(){/*{{{*/ + /* + * firstOrderingVerticesOnCell should order the vertices around a cell such that they are connected. + * i.e. verticesOnCell.at(iCell).at(i) should be the tail of a vector pointing to + * verticesOnCell.at(iCell).at(i+1) + * + * This is done by computing angles between vectors pointing from the cell center + * to each individual vertex. The next vertex in the list, has the smallest positive angle. + * + */ + + pnt vec1, vec2, cross, normal; + pnt vertex1, vertex2; + int iCell, iVertex1, iVertex2, j, k, l, swp_idx, swp; + double dot, mag1, mag2, angle, min_angle; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: firstOrderingVerticesOnCell" << endl << endl; +#endif + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } + + for(iCell = 0; iCell < cells.size(); iCell++){ +#ifdef _DEBUG + cout << "new cell: " << iCell << endl; + cout << " " << cells.at(iCell) << endl; +#endif + if(spherical){ + normal = cells.at(iCell); + } + +#ifdef _DEBUG + cout << " Unsorted verticesOnCell: "; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << verticesOnCell.at(iCell).at(j) << " "; + } + cout << endl; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; + for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ + cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; + } + cout << endl; + } +#endif + + // Loop over all vertices except the last two as a starting vertex + for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ + iVertex1 = verticesOnCell.at(iCell).at(j); + + vertex1 = vertices.at(iVertex1); + + //Check for, and fix up periodicity + if(!spherical){ + vertex1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + vec1 = vertex1 - cells.at(iCell); + + mag1 = vec1.magnitude(); + min_angle = 2.0*M_PI; + angle = 0.0; + swp_idx = -1; + + // Don't sort any vertices that have already been sorted. + for(k = j+1; k < verticesOnCell.at(iCell).size(); k++){ +#ifdef _DEBUG + cout << " Comparing " << j << " " << k << endl; +#endif + iVertex2 = verticesOnCell.at(iCell).at(k); + + vertex2 = vertices.at(iVertex2); + + //Check for, and fix up periodicity + if(!spherical){ + vertex2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + vec2 = vertex2 - cells.at(iCell); + + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); +#ifdef _DEBUG + cout << " dot = " << dot << endl; +#endif + + // Only look at vertices that are CCW from the current vertex. + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); +#ifdef _DEBUG + cout << " angle = " << angle << endl; +#endif + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } + + if(swp_idx != -1 && swp_idx != j+1){ + swp = verticesOnCell.at(iCell).at(j+1); + verticesOnCell.at(iCell).at(j+1) = verticesOnCell.at(iCell).at(swp_idx); + verticesOnCell.at(iCell).at(swp_idx) = swp; + } + +#ifdef _DEBUG + if(swp_idx != -1 && swp_idx != j+1){ + cout << " swapped " << j << " " << swp_idx << endl; + } else { + cout << " no swap" << endl; + } + cout << " cellsOnVertex(" << iVertex1 << "): "; + for(k = 0; k < cellsOnVertex.at(iVertex1).size(); k++){ + cout << cellsOnVertex.at(iVertex1).at(k) << " "; + } + cout << endl; + iVertex2 = verticesOnCell.at(iCell).at(j+1); + cout << " cellsOnVertex(" << iVertex2 << "): "; + for(k = 0; k < cellsOnVertex.at(iVertex2).size(); k++){ + cout << cellsOnVertex.at(iVertex2).at(k) << " "; + } + cout << endl; +#endif + } + +#ifdef _DEBUG + cout << " Sorted verticesOnCell: "; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << verticesOnCell.at(iCell).at(j) << " "; + } + cout << endl; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; + for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ + cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; + } + cout << endl; + } +#endif + } + + return 0; +}/*}}}*/ +int buildCompleteCellMask(){/*{{{*/ + /* + * The buildCompleteCellMask function parses an ordered + * verticesOnCell field to determine if a cell is complete. It takes each + * vertex-vertex pair (from verticesOnCell) and determines the angle + * between them. If only one vertex is shared, the "edge" is skipped. + * + * These angles are summed up around each cell. If the total + * angle is close to 2.0*Pi then the cell is marked as complete. Otherwise + * it is marked as incomplete. + * + * The complete check is used when building edges. If an edge only has one + * vertex, but is connected to two complete cells, the edge is not added to + * the set. This resolves an issue related to quad grids, where a possible + * edge connects two cells across a vertex (where the edge mid point would + * be the vertex location). Using this check removes these edges from the + * possible set of edges. + * + */ + int iCell, iCell2, vertex1, vertex2; + int j, k, l; + pnt vert_loc1, vert_loc2; + pnt vec1, vec2; + pnt cross, normal; + double angle, angle_sum, dot; + bool complete; + + completeCellMask.clear(); + completeCellMask.resize(cells.size()); + + if(!spherical) { + normal = pnt(0.0, 0.0, 1.0); + } + + // Iterate over all cells + for(iCell = 0; iCell < cells.size(); iCell++){ + complete = false; + angle_sum = 0.0; + + if(spherical){ + normal = cells.at(iCell); + } + +#ifdef _DEBUG + cout << " Checking " << iCell << " for completeness" << endl; + cout << " " << cells.at(iCell) << endl; +#endif + + // Since verticesOnEdge is ordered already, compute angles between + // neighboring vertex/vertex pairs if they are both valid vertices. Sum + // the angles, and if the angles are close to 2.0 * Pi then the cell is + // "complete" + if(verticesOnCell.at(iCell).size() >= cellsOnCell.at(iCell).size()){ + for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ + vertex1 = verticesOnCell.at(iCell).at(j); + vertex2 = verticesOnCell.at(iCell).at(j+1); + + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); + + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = vert_loc1 - cells.at(iCell); + vec2 = vert_loc2 - cells.at(iCell); + + cross = vec1.cross(vec2); + dot = cross.dot(normal); + +#ifdef _DEBUG + cout << " vec1 : " << vec1 << endl; + cout << " vec2 : " << vec2 << endl; + cout << " cross : " << cross << endl; + cout << " dot : " << dot << endl; +#endif + + if(dot > 0){ + + angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); + angle_sum += angle; + +#ifdef _DEBUG + cout << " adding angle (rad) : " << angle << endl; + cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; + cout << " new sum : " << angle_sum << endl; +#endif + } else { + cout << " complete check has non CCW edge..." << endl; + } + } + } + + vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); + vertex2 = verticesOnCell.at(iCell).at(0); + + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); + + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = vert_loc1 - cells.at(iCell); + vec2 = vert_loc2 - cells.at(iCell); + + cross = vec1.cross(vec2); + dot = cross.dot(normal); + +#ifdef _DEBUG + cout << " vec1 : " << vec1 << endl; + cout << " vec2 : " << vec2 << endl; + cout << " cross : " << cross << endl; + cout << " dot : " << dot << endl; +#endif + + if(dot > 0) { + + angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); + angle_sum += angle; + +#ifdef _DEBUG + cout << " adding angle (rad) : " << angle << endl; + cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; + cout << " new sum : " << angle_sum << endl; +#endif + } else { + cout << " complete check has non CCW edge..." << endl; + } + } + } + + if(angle_sum > 2.0 * M_PI * 0.98){ + complete = true; + } + +#ifdef _DEBUG + cout << " Vertices on cell: " << verticesOnCell.at(iCell).size(); + cout << " Cells on cell: " << cellsOnCell.at(iCell).size(); + if(complete){ + cout << " Is complete!" << endl; + } else { + cout << " Is not complete!" << endl; + } +#endif + + if(complete){ + completeCellMask.at(iCell) = 1; + } else { + completeCellMask.at(iCell) = 0; + } + } + + return 0; +}/*}}}*/ +int buildEdges(){/*{{{*/ + /* + * buildEdges is intended to build a hash table that contains the + * cellsOnEdge and verticesonEdge pairs for each edge. The actual edge + * location is not constructed at this point in time, as we need to + * determine the four constituent locations for each edge before we can + * compute it. + * + * This function assumes the cellsOnVertex array is ordered such that + * two consecutive cell indices make up an edge. This isn't an issue + * for triangular dual grids, but quadrilateral dual grids have an issue + * where every cell is not connected with every other cell. So, the tool + * that generates input data for this program needs to ensure that + * ordering. + * + * After the hash table is created, it is iterated over to create each + * individual edge and ensure ordering is properly right handed. + * + */ + unordered_set edge_idx_hash; + unordered_set::iterator edge_idx_itr; + unordered_set::const_iterator edge_idx_search; + + int iCell, iVertex, iEdge, j, k, l; + int cell1, cell2; + int vertex1, vertex2, swp; + int land; + pair::iterator,bool> out_pair; + edge new_edge; + pnt edge_loc, normal; + pnt cell_loc1, cell_loc2, vert_loc1, vert_loc2, dist_vec; + pnt u_vec, v_vec, cross; + double vert1_x_movement, vert1_y_movement; + double vert2_x_movement, vert2_y_movement; + double temp, dot; + bool fixed_edge, add_edge; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildEdges" << endl << endl; +#endif + + edge_idx_hash.clear(); + + land = 0; + + // Build all edges + for(iCell = 0; iCell < cells.size(); iCell++){ + if(completeCellMask.at(iCell) == 1) { + // Build edges from every vertex/vertex pair around a cell if the cell is complete + for(l = 0; l < verticesOnCell.at(iCell).size()-1; l++){ + vertex1 = verticesOnCell.at(iCell).at(l); + vertex2 = verticesOnCell.at(iCell).at(l+1); + + // Find cell shaerd by vertices, that's not iCell + cell1 = iCell; + cell2 = -1; + for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ + if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ + for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ + if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ + cell2 = cellsOnVertex.at(vertex1).at(j); + } + } + } + } + + add_edge = true; + +#ifdef _DEBUG + cout << "1 Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; +#endif + + if(vertex1 != -1 && vertex2 != -1){ + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); + + if(cell2 != -1){ + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } else { + new_edge.cell1 = cell1; + new_edge.cell2 = cell2; + } + + } else { + add_edge = false; + } + + if(add_edge){ +#ifdef _DEBUG + cout << " Adding edge" << endl; +#endif + out_pair = edge_idx_hash.insert(new_edge); + +#ifdef _DEBUG + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } +#endif + } else { +#ifdef _DEBUG + cout << " Not adding edge" << endl; +#endif + } + + } + + vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); + vertex2 = verticesOnCell.at(iCell).at(0); + + // Find cell shaerd by vertices, that's not iCell + cell1 = iCell; + cell2 = -1; + for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ + if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ + for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ + if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ + cell2 = cellsOnVertex.at(vertex1).at(j); + } + } + } + } + + add_edge = true; + +#ifdef _DEBUG + cout << " Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; +#endif + + if(vertex1 != -1 && vertex2 != -1){ + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); + + if(cell2 != -1){ + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } else { + new_edge.cell1 = cell1; + new_edge.cell2 = cell2; + } + + } else { + add_edge = false; + } + + if(add_edge){ +#ifdef _DEBUG + cout << " Adding edge" << endl; +#endif + out_pair = edge_idx_hash.insert(new_edge); +#ifdef _DEBUG + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } +#endif + } else { +#ifdef _DEBUG + cout << " Not adding edge" << endl; +#endif + } + + } else { + // Build edges from every cell/cell pair only if cell is not complete + for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ + cell1 = iCell; + cell2 = cellsOnCell.at(iCell).at(l); + + // Find vertex pair for cell pair + vertex1 = -1; + vertex2 = -1; + + for(j = 0; j < verticesOnCell.at(cell1).size(); j++){ + if(cell2 != -1){ + for(k = 0; k < verticesOnCell.at(cell2).size(); k++){ + if(verticesOnCell.at(cell1).at(j) == verticesOnCell.at(cell2).at(k)) { + if(vertex1 == -1){ + vertex1 = verticesOnCell.at(cell1).at(j); + } else if(vertex2 == -1) { + vertex2 = verticesOnCell.at(cell1).at(j); + } else { + cout << " Found more than 2 vertices for edge? " << endl; + } + } + } + } + } + + if(vertex2 == -1) { + new_edge.vertex1 = vertex1; + new_edge.vertex2 = vertex2; + } else { + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); + } + +#ifdef _DEBUG + cout << " Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; + + if(new_edge.vertex1 == -1){ + cout << " Edge is missing vertex 1" << endl; + } else if(new_edge.vertex2 == -1){ + cout << " Edge is missing vertex 2" << endl; + } +#endif + + if(cell2 == -1){ + new_edge.cell1 = cell1; + new_edge.cell2 = -1; + } else { + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } + add_edge = true; + + if(new_edge.vertex1 == -1 || new_edge.vertex2 == -1){ + if(new_edge.cell1 == -1 || new_edge.cell2 == -1){ + add_edge = false; + } + +#ifdef _DEBUG + cout << " Cell 1 complete: " << completeCellMask.at(new_edge.cell1) << endl; + if(new_edge.cell2 != -1) { + cout << " Cell 2 complete: " << completeCellMask.at(new_edge.cell2) << endl; + } +#endif + + if(completeCellMask.at(new_edge.cell1) != 0 || + (new_edge.cell2 != -1 && completeCellMask.at(new_edge.cell2) != 0) ){ + add_edge = false; + } + } + + + if(add_edge){ +#ifdef _DEBUG + cout << " Adding edge" << endl; +#endif + out_pair = edge_idx_hash.insert(new_edge); +#ifdef _DEBUG + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } +#endif + } else { +#ifdef _DEBUG + cout << " Not adding edge" << endl; +#endif + } + } + } + } + + cout << "Built " << edge_idx_hash.size() << " edge indices..." << endl; + + cellsOnEdge.clear(); + verticesOnEdge.clear(); + dvEdge.clear(); + dcEdge.clear(); + cellsOnEdge.resize(edge_idx_hash.size()); + verticesOnEdge.resize(edge_idx_hash.size()); + dvEdge.resize(edge_idx_hash.size()); + dcEdge.resize(edge_idx_hash.size()); + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } + + iEdge = 0; + for(edge_idx_itr = edge_idx_hash.begin(); edge_idx_itr != edge_idx_hash.end(); ++edge_idx_itr){ +#ifdef _DEBUG + cout << "new edge: " << endl; +#endif + fixed_edge = false; + cell1 = (*edge_idx_itr).cell1; + cell2 = (*edge_idx_itr).cell2; + vertex1 = (*edge_idx_itr).vertex1; + vertex2 = (*edge_idx_itr).vertex2; + + cell_loc1 = cells.at(cell1); + vert_loc1 = vertices.at(vertex1); + if(vertex2 != -1) { + vert_loc2 = vertices.at(vertex2); + } else { + vert_loc2 = vert_loc1; + } + + if(spherical){ + normal = cells.at(cell1); + } else { + // Clean up periodic edges. See mesh specification document for how periodicity is defined. + // These edges are special in the sense that they must be across a "uniform" edge. + // So, they are exactly the mid point between vertices. + // Since the edge will be "owned" by whatever processor owns cell1, make sure edge is close to cell1. + // So, fix periodicity relative to cell1. + vert_loc1.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + } + + if(cell2 != -1){ + cell_loc2 = cells.at(cell2); + if(vertex2 != -1){ + vert_loc2 = vertices.at(vertex2); + + if(!spherical) { + cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + edge_loc = planarIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); + dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); + dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); + } else { // If spherical + edge_loc = gcIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); + dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); + dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); + } + } else { + edge_loc = (cell_loc1 + cell_loc2) * 0.5; + vert_loc2 = edge_loc; + if(!spherical){ + dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); + dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); + } else { + dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); + dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); + } + } + } else { + if(vertex2 != -1){ + vert_loc2 = vertices.at(vertex2); + if(!spherical){ + // Set dv and dc Edge values on plane + vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + edge_loc = (vert_loc1 + vert_loc2) * 0.5; + dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); + dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); + } else { + // Set dv and dc Edge values on Sphere + edge_loc = (vert_loc1 + vert_loc2) * 0.5; + edge_loc.normalize(); + dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); + dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); + } + cell_loc2 = edge_loc; + } else { + cout << " ERROR: Edge found with only 1 cell and 1 vertex...." << endl; + return 1; + } + } + + if(spherical) edge_loc.normalize(); + + edge_loc.idx = iEdge; + +#ifdef _DEBUG + cout << "New Edge At: " << edge_loc << endl; + cout << " c1: " << cells.at(cell1) << endl; + if(cell2 > -1) { + cout << " c2: " << cells.at(cell2) << endl; + cout << " mod? c2: " << cell_loc2 << endl; + } else { + cout << " c2: land" << endl; + cout << " mod? c2: " << cell_loc2 << endl; + } + cout << " v1: " << vertices.at(vertex1) << endl; + cout << " mod? v1: " << vert_loc1 << endl; + if(vertex2 != -1) { + cout << " v2: " << vertices.at(vertex2) << endl; + cout << " mod? v2: " << vert_loc2 << endl; + } else { + cout << " v2: land" << endl; + cout << " mod? v2: " << vert_loc2 << endl; + } +#endif + + u_vec = cell_loc2 - cell_loc1; + v_vec = vert_loc2 - vert_loc1; + + cross = u_vec.cross(v_vec); + dot = cross.dot(normal); + + if(dot < 0){ + if(vertex2 != -1){ +#ifdef _DEBUG + cout << " swapping vertex " << vertex1 << " and " << vertex2 << endl; +#endif + swp = vertex2; + vertex2 = vertex1; + vertex1 = swp; + } else { +#ifdef _DEBUG + cout << " swapping cell " << cell1 << " and " << cell2 << endl; +#endif + swp = cell2; + cell2 = cell1; + cell1 = swp; + } + } + + edges.push_back(edge_loc); + cellsOnEdge.at(iEdge).clear(); + cellsOnEdge.at(iEdge).push_back(cell1); + cellsOnEdge.at(iEdge).push_back(cell2); + verticesOnEdge.at(iEdge).clear(); + verticesOnEdge.at(iEdge).push_back(vertex1); + verticesOnEdge.at(iEdge).push_back(vertex2); + + iEdge++; + } + + edge_idx_hash.clear(); + + return 0; +}/*}}}*/ +int orderVertexArrays(){/*{{{*/ + /* + * orderVertexArrays builds and orders the connectivity arrays for vertices. + * This includes edgesOnVertex and cellsOnvertex. + * First, edgeSOnVertex is built and ordered correctly, then using + * that ordering cellsOnVertex is built and ordered correctly as well. + */ + int iEdge, iVertex, vertex1, vertex2, cell1, cell2, edge1, edge2; + int i, j, k, l, swp_idx, swp; + pnt normal; + pnt cross; + pnt vec1, vec2; + pnt cell_loc, edge_loc1, edge_loc2; + double mag1, mag2, min_angle, angle; + double dvEdge, dot, area; + bool fixed_area; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: orderVertexArrays" << endl << endl; +#endif + + edgesOnVertex.clear(); + edgesOnVertex.resize(vertices.size()); + cellsOnVertex.clear(); + cellsOnVertex.resize(vertices.size()); + + // Get lists of edges for each vertex + for(iEdge = 0; iEdge < edges.size(); iEdge++){/*{{{*/ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + + if(vertex1 != -1) { + edgesOnVertex.at(vertex1).push_back(iEdge); + } + + if(vertex2 != -1){ + edgesOnVertex.at(vertex2).push_back(iEdge); + } + }/*}}}*/ + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } + + // Order edges counter-clockwise + for(iVertex = 0; iVertex < vertices.size(); iVertex++){/*{{{*/ + if(spherical){ + normal = vertices.at(iVertex); + } + + //Loop over all edges except the last two as a starting edge. + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + edge1 = edgesOnVertex.at(iVertex).at(j); + + edge_loc1 = edges.at(edge1); + + // Fix edge1 periodicity + if(!spherical){ + edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + } + vec1 = edge_loc1 - vertices.at(iVertex); + + mag1 = vec1.magnitude(); + min_angle = 2.0*M_PI; + angle = 0.0; + swp_idx = -1; + + //Don't sort any edges that have already been sorted. + for(k = j+1; k < edgesOnVertex.at(iVertex).size(); k++){ + edge2 = edgesOnVertex.at(iVertex).at(k); + + edge_loc2 = edges.at(edge2); + + // Fix edge2 periodicity + if(!spherical){ + edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + } + vec2 = edge_loc2 - vertices.at(iVertex); + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + + // Only look at vertices that are CCW from the current edge. + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } + + if(swp_idx != -1 && swp_idx != j+1){ + swp = edgesOnVertex.at(iVertex).at(j+1); + edgesOnVertex.at(iVertex).at(j+1) = edgesOnVertex.at(iVertex).at(swp_idx); + edgesOnVertex.at(iVertex).at(swp_idx) = swp; + } + } + +#ifdef _DEBUG + cout << "edgesOnVertex("<< iVertex <<"): "; + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + cout << edgesOnVertex.at(iVertex).at(j) << " "; + } + cout << endl; +#endif + + cellsOnVertex.at(iVertex).clear(); +#ifdef _DEBUG + cout << "CellsOnVertex("<< iVertex << "): "; +#endif + + // Using the ordered edges. Buld cellsOnVertex in the correct order. + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + edge1 = edgesOnVertex.at(iVertex).at(j); + fixed_area = false; + + // Get cell id and add it to list of cells + if(iVertex == verticesOnEdge.at(edge1).at(0)){ + cell1 = cellsOnEdge.at(edge1).at(0); + cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(0) ); +#ifdef _DEBUG + cout << cellsOnEdge.at(edge1).at(0) << " "; +#endif + } else { + cell1 = cellsOnEdge.at(edge1).at(1); + cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(1) ); +#ifdef _DEBUG + cout << cellsOnEdge.at(edge1).at(1) << " "; +#endif + } + } +#ifdef _DEBUG + cout << endl << endl; +#endif + }/*}}}*/ + + return 0; +}/*}}}*/ +int orderCellArrays(){/*{{{*/ + /* + * orderCellArrays assumes verticesOnCell are ordered CCW already. + * + */ + int iCell, iVertex, iEdge, iEdge2, add_cell; + int cell1, cell2, vertex1, vertex2, prev_vertex; + int edge_idx, loc_edge_idx; + int i, j, k, swp_idx; + pnt normal, cross; + pnt vec1, vec2; + pnt vert_loc, edge_loc, cell_loc; + pnt edge_loc1, edge_loc2; + pnt next_edge_loc; + double swp; + double dot, mag1, mag2, angle, min_angle; + bool found, bad_vertices; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: orderCellArrays" << endl << endl; +#endif + + maxEdges = 0; + + verticesOnCell.clear(); + edgesOnCell.clear(); + cellsOnCell.clear(); + + verticesOnCell.resize(cells.size()); + edgesOnCell.resize(cells.size()); + cellsOnCell.resize(cells.size()); + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } + + // First, build full list of edges on cell. + for(iEdge = 0; iEdge < edges.size(); iEdge++){ + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + + edgesOnCell.at(cell1).push_back(iEdge); + if(cell2 != -1){ + edgesOnCell.at(cell2).push_back(iEdge); + } + } + + // Loop over all cells. + for(iCell = 0; iCell < cells.size(); iCell++){ +#ifdef _DEBUG + cout << "New Cell " << cells.at(iCell) << endl; +#endif + if(spherical){ + normal = cells.at(iCell); + } + + loc_edge_idx = 0; // if no edges on cell, apparently occurs for quad meshes + if ( edgesOnCell.at(iCell).size() != 0 ) { +#ifdef _DEBUG + cout << endl; + cout << " Starting edgesOnCell on cell: " << iCell << endl; + cout << " Starting edgesOnCell size: " << edgesOnCell.at(iCell).size() << endl; + cout << " Starting edgesOnCell: "; + for(i = 0; i < edgesOnCell.at(iCell).size(); ++i){ + cout << edgesOnCell.at(iCell).at(i) << " "; + } + cout << endl; +#endif + + // /* + // Determine starting edge. It should either be the first edge in the set, + // or the only edge such that all other edges are CCW from it. + edge_idx = edgesOnCell.at(iCell).at(0); + #ifdef _DEBUG + cout << "Finding starting edge for cell " << iCell << endl; + #endif + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + + if(vertex2 == -1){ + #ifdef _DEBUG + cout << " Start edge: " << iEdge << endl; + cout << " " << edges.at(iEdge) << endl; + cout << " v1: " << vertex1 << endl; + cout << " v2: " << vertex2 << endl; + #endif + edge_loc1 = edges.at(iEdge); + edge_loc1 = vertices.at(vertex1); + if(!spherical){ + edge_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = edge_loc1 - cells.at(iCell); + mag1 = vec1.magnitude(); + + // If edge only has one vertex. Need to find edge that shares the vertex. + // This edge is kept if the neighboring edge is CCW from it. + for(k = 0; k < edgesOnCell.at(iCell).size(); k++){ + if(j != k){ + iEdge2 = edgesOnCell.at(iCell).at(k); + #ifdef _DEBUG + cout << " Test edge: " << iEdge2 << endl; + cout << " " << edges.at(iEdge2) << endl; + cout << " v1: " << verticesOnEdge.at(iEdge2).at(0) << endl; + cout << " v2: " << verticesOnEdge.at(iEdge2).at(1) << endl; + #endif + + if(vertex1 == verticesOnEdge.at(iEdge2).at(0) || vertex1 == verticesOnEdge.at(iEdge2).at(1)){ + // This edge is a neighboring edge. Check for CCW ordering. + if(vertex1 == verticesOnEdge.at(iEdge2).at(0)) { + vertex2 = verticesOnEdge.at(iEdge2).at(1); + } else { + vertex2 = verticesOnEdge.at(iEdge2).at(0); + } + edge_loc2 = edges.at(iEdge2); + edge_loc2 = vertices.at(vertex2); + + if(!spherical){ + edge_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec2 = edge_loc2 - cells.at(iCell); + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + + #ifdef _DEBUG + cout << " Vec1: " << vec1 << endl; + cout << " Vec2: " << vec2 << endl; + cout << " Cross: " << cross << endl; + cout << " dot: " << dot << endl; + #endif + if(dot > 0){ + #ifdef _DEBUG + cout << " Found edge: " << iEdge << endl; + #endif + edge_idx = iEdge; + loc_edge_idx = j; + } + } + } + } + } + } + } + + // Swap edge_idx with first edge. + if(loc_edge_idx != 0){ +#ifdef _DEBUG + cout << " Swapping edges: " << + edgesOnCell.at(iCell).at(loc_edge_idx) << + " and " << edgesOnCell.at(iCell).at(0) << endl; +#endif + edgesOnCell.at(iCell).at(loc_edge_idx) = edgesOnCell.at(iCell).at(0); + edgesOnCell.at(iCell).at(0) = edge_idx; + } + // */ + + // Order all edges in CCW relative to the first edge. + // Loop over all vertices except the last two as a starting vertex. + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + edge_loc = edges.at(iEdge); + + // Add cell and vertex from first edge. + // Add cell across edge to cellsOnCell + // Also, add vertex that is CCW relative to current edge location. + if(cellsOnEdge.at(iEdge).at(0) == iCell){ + cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(1)); + + if(verticesOnEdge.at(iEdge).at(1) != -1){ + verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(1)); + } + } else { + cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(0)); + verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(0)); + } + + + if(!spherical){ + edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = edge_loc - cells.at(iCell); + mag1 = vec1.magnitude(); + + min_angle = 2.0 * M_PI; + angle = 0.0; + swp_idx = -1; + + for(k = j+1; k < edgesOnCell.at(iCell).size(); k++){ + iEdge2 = edgesOnCell.at(iCell).at(k); + + next_edge_loc = edges.at(iEdge2); + + if(!spherical){ + next_edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec2 = next_edge_loc - cells.at(iCell); + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + + // Only look at edges that are CCW from the current edge + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } + + if(swp_idx != -1 && swp_idx != j+1){ + swp = edgesOnCell.at(iCell).at(j+1); + edgesOnCell.at(iCell).at(j+1) = edgesOnCell.at(iCell).at(swp_idx); + edgesOnCell.at(iCell).at(swp_idx) = swp; + } + } + + +#ifdef _DEBUG + cout << " cellsOnCell: "; + for(i = 0; i < cellsOnCell.at(iCell).size(); i++){ + cout << cellsOnCell.at(iCell).at(i) << " "; + } + cout << endl; + cout << " verticesOnCell: "; + for(i = 0; i < verticesOnCell.at(iCell).size(); i++){ + cout << verticesOnCell.at(iCell).at(i) << " "; + } + cout << endl; + cout << " edgesOnCell: "; + for(i = 0; i < edgesOnCell.at(iCell).size(); i++){ + cout << edgesOnCell.at(iCell).at(i) << " "; + } + cout << endl; +#endif + + maxEdges = max(maxEdges, (int)edgesOnCell.at(iCell).size()); + } + + return 0; +}/*}}}*/ +int buildAreas(){/*{{{*/ + /* + * buildAreas constructs the area arrays. + * This includes areaCell, areaTriangle, and kiteAreasOnVertex + * + * Before constructing areaCell, the cell is checked for completeness. + * This is accomplished by looping over each edge. For each edge, + * the angle between it's vertices is computed and added to a running total. + * If the total is significantly less than 2*Pi, the cell is not complete. + * + * Non-complete cells are given a negative area, to ease removal at a later stage. + */ + int iVertex, iCell, iEdge, i, j; + int vertex1, vertex2; + int edge1, edge2; + int incomplete_cells; + pnt vert_loc1, vert_loc2; + pnt edge_loc1, edge_loc2; + pnt cell_loc; + pnt vec1, vec2; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildAreas" << endl << endl; +#endif + + areaCell.clear(); + areaTriangle.clear(); + kiteAreasOnVertex.clear(); + + areaCell.resize(cells.size()); + areaTriangle.resize(vertices.size()); + kiteAreasOnVertex.resize(vertices.size()); + + incomplete_cells = 0; + + for(iCell = 0; iCell < cells.size(); iCell++){ + areaCell.at(iCell) = 0.0; + + if(completeCellMask.at(iCell) == 1){ + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + + if(cellsOnEdge.at(iEdge).at(0) == iCell){ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + } else { + vertex1 = verticesOnEdge.at(iEdge).at(1); + vertex2 = verticesOnEdge.at(iEdge).at(0); + } + + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); + + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + areaCell.at(iCell) += planarTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); + } else { + areaCell.at(iCell) += sphericalTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); + } + } + } + } else { + incomplete_cells++; +#ifdef _DEBUG + cout << " Non complete cell found at " << iCell << endl; +#endif + areaCell.at(iCell) = -1; + } + } + + for(iVertex = 0; iVertex < vertices.size(); iVertex++){ + areaTriangle.at(iVertex) = 0.0; + kiteAreasOnVertex.at(iVertex).resize(cellsOnVertex.at(iVertex).size()); + for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + kiteAreasOnVertex.at(iVertex).at(j) = 0.0; + iCell = cellsOnVertex.at(iVertex).at(j); + + if(iCell != -1){ + edge1 = edgesOnVertex.at(iVertex).at(j); + + if(j == cellsOnVertex.at(iVertex).size()-1){ + edge2 = edgesOnVertex.at(iVertex).at(0); + } else { + edge2 = edgesOnVertex.at(iVertex).at(j+1); + } + + cell_loc = cells.at(iCell); + edge_loc1 = edges.at(edge1); + edge_loc2 = edges.at(edge2); + + if(!spherical){ + cell_loc.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + kiteAreasOnVertex.at(iVertex).at(j) += + planarTriangleArea(vertices.at(iVertex), edge_loc1, cell_loc); + kiteAreasOnVertex.at(iVertex).at(j) += + planarTriangleArea(vertices.at(iVertex), cell_loc, edge_loc2); + } else { + kiteAreasOnVertex.at(iVertex).at(j) += + sphericalTriangleArea(vertices.at(iVertex), edge_loc1, cell_loc); + kiteAreasOnVertex.at(iVertex).at(j) += + sphericalTriangleArea(vertices.at(iVertex), cell_loc, edge_loc2); + } + + areaTriangle.at(iVertex) += kiteAreasOnVertex.at(iVertex).at(j); + } + } + } + + cout << " Found " << incomplete_cells << " incomplete cells. Each is marked with an area of -1." << endl; + + return 0; +}/*}}}*/ +int buildEdgesOnEdgeArrays(){/*{{{*/ + /* + * buildEdgesOnEdgeArrays builds both edgesOnEdge and weightsOnEdge + * + * The weights correspond to edgesOnEdge and allow MPAS to reconstruct the + * edge perpendicular (previously defined as v) velocity using the edge + * neighbors + * + * Weight formulation is defined in J. Thurburn, et al. JCP 2009 + * Numerical representation of geostrophic modes on arbitrarily + * structured C-grids + * + * Before using this function, dcEdge, dvEdge, areaCell, and kiteAreasOnVertex + * all need to be computed correctly. + */ + int iEdge, iCell, cell1, cell2; + int i, j, k; + int shared_vertex; + int last_edge, cur_edge; + double area_sum; + bool found; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildEdgesOnEdgeArrays" << endl << endl; +#endif + + edgesOnEdge.clear(); + edgesOnEdge.resize(edges.size()); + + weightsOnEdge.clear(); + weightsOnEdge.resize(edges.size()); + + for(iEdge = 0; iEdge < edges.size(); iEdge++){ +#ifdef _DEBUG + cout << "New edge: " << edges.at(iEdge) << endl; +#endif + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + found = false; + + // Loop over cell 1. Starting from the edge after the current edge, add + // all edges counter clockwise around the cell. Don't add the current + // edge to the list. +#ifdef _DEBUG + cout << " On cell1: " << cell1 << endl; +#endif + last_edge = iEdge; + area_sum = 0; + for(i = 0; i < edgesOnCell.at(cell1).size(); i++){ +#ifdef _DEBUG + cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; +#endif + if(edgesOnCell.at(cell1).at(i) == iEdge){ + found = true; +#ifdef _DEBUG + cout << " -- found -- " << endl; +#endif + } + + if(found && edgesOnCell.at(cell1).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell1).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); + + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(0); + + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } + + if(shared_vertex != -1) { + // Find cell 1 on shared vertex (to get kite area) + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); + + if(iCell == cell1){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); + } + } + } + + if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( + +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } else { + weightsOnEdge.at(iEdge).push_back( + -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } + + last_edge = edgesOnCell.at(cell1).at(i); +#ifdef _DEBUG + cout << " added " << edgesOnCell.at(cell1).at(i) << endl; +#endif + } + } + if(!found) { + cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; + return 1; + } + + for(i = 0; i < edgesOnCell.at(cell1).size() && found; i++){ +#ifdef _DEBUG + cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; +#endif + if(edgesOnCell.at(cell1).at(i) == iEdge){ +#ifdef _DEBUG + cout << " -- found -- " << endl; +#endif + found = false; + } + + if(found && edgesOnCell.at(cell1).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell1).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); + + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(0); + + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } + + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); + + if(iCell == cell1){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); + } + } + } + + if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( + +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } else { + weightsOnEdge.at(iEdge).push_back( + -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } + + last_edge = edgesOnCell.at(cell1).at(i); +#ifdef _DEBUG + cout << " added " << edgesOnCell.at(cell1).at(i) << endl; +#endif + } + } + if(found) { + cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; + return 1; + } + + // Check if cell1 is a real cell or not. + if(cell2 > -1){ + last_edge = iEdge; + area_sum = 0.0; +#ifdef _DEBUG + cout << " On cell2: " << cell2 << endl; +#endif + found = false; + // Loop over cell 2. Starting from the edge after the current edge, + // add all edges counter clockwise around the cell. Don't add the + // current edge to the list. + for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ +#ifdef _DEBUG + cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; +#endif + if(edgesOnCell.at(cell2).at(i) == iEdge){ + found = true; +#ifdef _DEBUG + cout << " -- found -- " << endl; +#endif + } + + if(found && edgesOnCell.at(cell2).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell2).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); + + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(0); + + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } + + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); + + if(iCell == cell2){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); + } + } + } + + if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( + -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } else { + weightsOnEdge.at(iEdge).push_back( + +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } + + last_edge = edgesOnCell.at(cell2).at(i); +#ifdef _DEBUG + cout << " added " << edgesOnCell.at(cell2).at(i) << endl; +#endif + } + } + if(!found) { + cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; + return 1; + } + + for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ +#ifdef _DEBUG + cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; +#endif + if(edgesOnCell.at(cell2).at(i) == iEdge){ + found = false; +#ifdef _DEBUG + cout << " -- found -- " << endl; +#endif + } + + if(found && edgesOnCell.at(cell2).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell2).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); + + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(0); + + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } + + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); + + if(iCell == cell2){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); + } + } + } + + if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( + -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } else { + weightsOnEdge.at(iEdge).push_back( + +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); + } + + last_edge = edgesOnCell.at(cell2).at(i); +#ifdef _DEBUG + cout << " added " << edgesOnCell.at(cell2).at(i) << endl; +#endif + } + } + if(found) { + cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; + return 1; + } + } + } + + return 0; +}/*}}}*/ +int buildAngleEdge(){/*{{{*/ + /* + * buildAngleEdge constructs angle edge for each edge. + * + * angleEdge is either: + * 1. The angle the positive tangential direction (v) + * makes with the local northward direction. + * or + * 2. The angles the positive normal direction (u) + * makes with the local eastward direction. + * + * In a plane, local eastward is defined as the x axis, and + * nortward is defined as the y axis. + */ + int iEdge; + int cell1, cell2; + int vertex1, vertex2; + pnt np, x_axis, normal; + pnt cell_loc1, cell_loc2; + pnt vertex_loc1, vertex_loc2; + double angle, sign; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildAngleEdge" << endl << endl; +#endif + + angleEdge.clear(); + angleEdge.resize(edges.size()); + + x_axis = pnt(1.0, 0.0, 0.0); + + for(iEdge = 0; iEdge < edges.size(); iEdge++){ + +#ifdef _DEBUG + cout << "New edge: " << edges.at(iEdge) << endl; +#endif + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + + cell_loc1 = cells.at(cell1); + if(cell2 != -1){ + cell_loc2 = cells.at(cell2); + } else { + cell_loc2 = edges.at(iEdge); + } + + vertex_loc1 = vertices.at(vertex1); + if(vertex2 != -1){ + vertex_loc2 = vertices.at(vertex2); + } else { + vertex_loc2 = edges.at(iEdge); + } + + if(!spherical){ + cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + + normal = cell_loc2 - cell_loc1; + angleEdge.at(iEdge) = acos( x_axis.dot(normal) / (x_axis.magnitude() * normal.magnitude())); + if (cell_loc2.y < cell_loc1.y) angleEdge.at(iEdge) = 2.0 * M_PI - angleEdge.at(iEdge); + } else { + + np = pntFromLatLon(edges.at(iEdge).getLat()+0.05, edges.at(iEdge).getLon()); + np.normalize(); + +#ifdef _DEBUG + cout << " NP: " << np << endl; +#endif + + angle = (vertex_loc2.getLat() - vertex_loc1.getLat()) / dvEdge.at(iEdge); + angle = max( min(angle, 1.0), -1.0); + angle = acos(angle); + +#ifdef _DEBUG + cout << " angle: " << angle << endl; +#endif + + sign = planeAngle(edges.at(iEdge), np, vertex_loc2, edges.at(iEdge)); + if(sign != 0.0){ + sign = sign / fabs(sign); + } else { + sign = 1.0; + } + + +#ifdef _DEBUG + cout << " sign : " << sign << endl; + cout << " a*s : " << angle * sign << endl; +#endif + + angle = angle * sign; + if(angle > M_PI) angle = angle - 2.0 * M_PI; + if(angle < -M_PI) angle = angle + 2.0 * M_PI; + +#ifdef _DEBUG + cout << " fangle: " << angle << endl; +#endif + + angleEdge.at(iEdge) = angle; + } + } + + return 0; +}/*}}}*/ +int buildMeshQualities(){/*{{{*/ + /* + * buildMeshQualities constructs fields describing the quality of the mesh, including: + * - cellQuality: a double between 0 and 1 describing how uniform the cell is + * - gridSpacing: an estimate of the grid spacing of each cell + * - triangleQuality: a double between 0 and 1 describing how uniform the cell is based on edge lenghts + * - triangleAngleQuality: a double between 0 and 1 describing how uniform the cell is based on the angles of the triangle + * - obtuseTriangle: an integer either 0 or 1 that flags triangles with an obtuse angle (set to 1) + */ + + int iCell, iVertex, iEdge; + int i, j; + double maxEdge, minEdge, spacing; + double angle, maxAngle, minAngle; + int obtuse; + + cellQuality.clear(); + gridSpacing.clear(); + triangleQuality.clear(); + triangleAngleQuality.clear(); + obtuseTriangle.clear(); + + cellQuality.resize(cells.size()); + gridSpacing.resize(cells.size()); + + triangleQuality.resize(vertices.size()); + triangleAngleQuality.resize(vertices.size()); + obtuseTriangle.resize(vertices.size()); + +#ifdef _DEBUG + cout << "Starting mesh quality calcs." < 0.0) { + cellQuality.at(iCell) = minEdge / maxEdge; + } else { + cellQuality.at(iCell) = 0.0; + } + gridSpacing.at(iCell) = spacing / edgesOnCell.at(iCell).size(); + } + +#ifdef _DEBUG + cout << "Starting loop over vertices." < M_PI_2 ) { + obtuse = 1; + obtuseTriangles++; + } + + triangleAngleQuality.at(iVertex) = minAngle / maxAngle; + obtuseTriangle.at(iVertex) = obtuse; + } else { + triangleAngleQuality.at(iVertex) = 1.0; + obtuseTriangle.at(iVertex) = 0; + } + } + } + } + + cout << "\tMesh contains: " << obtuseTriangles << " obtuse triangles." << endl; + + return 0; +}/*}}}*/ +/*}}}*/ + +/* Output functions {{{*/ +int outputGridDimensions( const string outputFilename ){/*{{{*/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + int grid, retv; + if ((retv = nc_create(outputFilename.c_str(), NC_CLOBBER|NC_NETCDF4, &grid))) + { + std::cout << "Can't create file: " << outputFilename << std::endl; + return retv ; + } + + nCells = cells.size(); + + /* + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + maxEdges = std::max(maxEdges, (int)(*vec_int_itr).size()); + }*/ + + ncutil::def_dim(outputFilename, "nCells", cells.size()); + ncutil::def_dim(outputFilename, "nEdges", edges.size()); + ncutil::def_dim(outputFilename, "nVertices", vertices.size()); + ncutil::def_dim(outputFilename, "maxEdges", maxEdges); + ncutil::def_dim(outputFilename, "maxEdges2", maxEdges * 2); + ncutil::def_dim(outputFilename, "TWO", 2); + ncutil::def_dim(outputFilename, "vertexDegree", vertex_degree); + ncutil::def_dim(outputFilename, "Time", NC_UNLIMITED); + + return 0; +}/*}}}*/ +int outputGridAttributes( const string outputFilename, const string inputFilename ){/*{{{*/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + char mesh_spec_str[1024]; + + string history_str = ""; + string id_str = ""; + string parent_str =""; + + // write attributes + if(!spherical){ + ncutil::put_str(outputFilename, "on_a_sphere", "NO"); + ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, 0.); + } else { + ncutil::put_str(outputFilename, "on_a_sphere", "YES"); + ncutil::put_att(outputFilename, + "sphere_radius", NC_DOUBLE, sphereRadius); + } + + if(!periodic){ + ncutil::put_str(outputFilename, "is_periodic", "NO"); + } else { + ncutil::put_str(outputFilename, "is_periodic", "YES"); + ncutil::put_att(outputFilename, "x_period", NC_DOUBLE, xPeriod); + ncutil::put_att(outputFilename, "y_period", NC_DOUBLE, yPeriod); + } + + history_str += "MpasMeshConverter.x "; + history_str += inputFilename; + history_str += " "; + history_str += outputFilename; + if(in_history != ""){ + history_str += "\n"; + history_str += in_history; + } + + if(in_file_id != "" ){ + parent_str = in_file_id; + if(in_parent_id != ""){ + parent_str += "\n"; + parent_str += in_parent_id; + } + ncutil::put_str(outputFilename, "parent_id", parent_str); + } + id_str = gen_random(ID_LEN); + + sprintf(mesh_spec_str, "%2.1lf", (double)MESH_SPEC); + + ncutil::put_str(outputFilename, "history", history_str); + ncutil::put_str(outputFilename, "mesh_spec", mesh_spec_str); + ncutil::put_str(outputFilename, "Conventions", "MPAS"); + ncutil::put_str(outputFilename, "source", "MpasMeshConverter.x"); + ncutil::put_str(outputFilename, "file_id", id_str); + + return 0; +}/*}}}*/ +int outputGridCoordinates( const string outputFilename) {/*{{{*/ + /************************************************************************ + * + * This function writes the grid coordinates to the netcdf file named + * outputFilename + * This includes all cell centers, vertices, and edges. + * Both cartesian and lat,lon, as well as all of their indices + * + * **********************************************************************/ + + int nCells = cells.size(); + int nEdges = edges.size(); + int nVertices = vertices.size(); + + int i; + double *x, *y, *z, *lat, *lon; + int *idxTo; + + // Build and write cell coordinate arrays + x = new double[nCells]; + y = new double[nCells]; + z = new double[nCells]; + lat = new double[nCells]; + lon = new double[nCells]; + idxTo = new int[nCells]; + i = 0; + for(pnt_itr = cells.begin(); pnt_itr != cells.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latCell", + NC_DOUBLE, "latitudes of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "lonCell", + NC_DOUBLE, "longitudes of cell centres", {"nCells"}); + + ncutil::put_var(outputFilename, "latCell", &lat[0]); + ncutil::put_var(outputFilename, "lonCell", &lon[0]); + + ncutil::def_var(outputFilename, "xCell", + NC_DOUBLE, "x-coordinates of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "yCell", + NC_DOUBLE, "y-coordinates of cell centres", {"nCells"}); + ncutil::def_var(outputFilename, "zCell", + NC_DOUBLE, "z-coordinates of cell centres", {"nCells"}); + + ncutil::put_var(outputFilename, "xCell", &x[0]); + ncutil::put_var(outputFilename, "yCell", &y[0]); + ncutil::put_var(outputFilename, "zCell", &z[0]); + + ncutil::def_var(outputFilename, "indexToCellID", + NC_INT, "index to cell ID mapping", {"nCells"}); + + ncutil::put_var(outputFilename, "indexToCellID", &idxTo[0]); + + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; + + //Build and write edge coordinate arrays + x = new double[nEdges]; + y = new double[nEdges]; + z = new double[nEdges]; + lat = new double[nEdges]; + lon = new double[nEdges]; + idxTo = new int[nEdges]; + + i = 0; + for(pnt_itr = edges.begin(); pnt_itr != edges.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latEdge", + NC_DOUBLE, "latitudes of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "lonEdge", + NC_DOUBLE, "longitudes of edge centres", {"nEdges"}); + + ncutil::put_var(outputFilename, "latEdge", &lat[0]); + ncutil::put_var(outputFilename, "lonEdge", &lon[0]); + + ncutil::def_var(outputFilename, "xEdge", + NC_DOUBLE, "x-coordinates of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "yEdge", + NC_DOUBLE, "y-coordinates of edge centres", {"nEdges"}); + ncutil::def_var(outputFilename, "zEdge", + NC_DOUBLE, "z-coordinates of edge centres", {"nEdges"}); + + ncutil::put_var(outputFilename, "xEdge", &x[0]); + ncutil::put_var(outputFilename, "yEdge", &y[0]); + ncutil::put_var(outputFilename, "zEdge", &z[0]); + + ncutil::def_var(outputFilename, "indexToEdgeID", + NC_INT, "index to edge ID mapping", {"nEdges"}); + + ncutil::put_var(outputFilename, "indexToEdgeID", &idxTo[0]); + + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; + + //Build and write vertex coordinate arrays + x = new double[nVertices]; + y = new double[nVertices]; + z = new double[nVertices]; + lat = new double[nVertices]; + lon = new double[nVertices]; + idxTo = new int[nVertices]; + + i = 0; + for(pnt_itr = vertices.begin(); pnt_itr != vertices.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latVertex", + NC_DOUBLE, "latitudes of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "lonVertex", + NC_DOUBLE, "longitudes of vertices", {"nVertices"}); + + ncutil::put_var(outputFilename, "latVertex", &lat[0]); + ncutil::put_var(outputFilename, "lonVertex", &lon[0]); + + ncutil::def_var(outputFilename, "xVertex", + NC_DOUBLE, "x-coordinates of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "yVertex", + NC_DOUBLE, "y-coordinates of vertices", {"nVertices"}); + ncutil::def_var(outputFilename, "zVertex", + NC_DOUBLE, "z-coordinates of vertices", {"nVertices"}); + + ncutil::put_var(outputFilename, "xVertex", &x[0]); + ncutil::put_var(outputFilename, "yVertex", &y[0]); + ncutil::put_var(outputFilename, "zVertex", &z[0]); + + ncutil::def_var(outputFilename, "indexToVertexID", + NC_INT, "index to vertex ID mapping", {"nVertices"}); + + ncutil::put_var(outputFilename, "indexToVertexID", &idxTo[0]); + + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; + + return 0; +}/*}}}*/ +int outputCellConnectivity( const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function writes all of the *OnCell arrays. Including + * cellsOnCell + * edgesOnCell + * verticesOnCell + * nEdgesonCell + * + * ***************************************************************/ + + int nCells = cells.size(); + int i, j; + + int *tmp_arr; + + // Build and write COC array + tmp_arr = new int[nCells*maxEdges]; + + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = cellsOnCell.begin(); vec_int_itr != cellsOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "cellsOnCell", + NC_INT, "cells adj. to each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "cellsOnCell", &tmp_arr[0]); + + // Build and write EOC array + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnCell", + NC_INT, "edges on each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "edgesOnCell", &tmp_arr[0]); + + // Build and write VOC array + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = verticesOnCell.begin(); vec_int_itr != verticesOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "verticesOnCell", + NC_INT, "vertices on each cell", {"nCells", "maxEdges"}); + + ncutil::put_var(outputFilename, "verticesOnCell", &tmp_arr[0]); + + delete[] tmp_arr; + + //Build and write nEOC array + tmp_arr = new int[nCells]; + + i = 0; + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + tmp_arr[i] = (*vec_int_itr).size(); + i++; + } + + ncutil::def_var(outputFilename, "nEdgesOnCell", + NC_INT, "number of edges on each cell", {"nCells"}); + + ncutil::put_var(outputFilename, "nEdgesOnCell", &tmp_arr[0]); + + verticesOnCell.clear(); + edgesOnCell.clear(); + + delete[] tmp_arr; + + return 0; +}/*}}}*/ +int outputEdgeConnectivity( const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function writes all of the *OnEdge arrays. Including + * cellsOnEdge + * edgesOnEdge + * verticesOnEdge + * nEdgesOnEdge + * + * ***************************************************************/ + + int nEdges = edges.size(); + int maxEdges2 = maxEdges * 2; + int vertexDegree = vertex_degree; + int two = 2; + int i, j; + + int *tmp_arr; + + // Build and write EOE array + tmp_arr = new int[nEdges*maxEdges2]; + + for(i = 0; i < nEdges; i++){ + for(j = 0; j < maxEdges2; j++){ + tmp_arr[i*maxEdges2 + j] = 0; + } + } + + i = 0; + for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges2 + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnEdge", + NC_INT, "edges adj. to each edge", {"nEdges", "maxEdges2"}); + + ncutil::put_var(outputFilename, "edgesOnEdge", &tmp_arr[0]); + + delete[] tmp_arr; + + // Build and write COE array + tmp_arr = new int[nEdges*two]; + for(i = 0; i < nEdges; i++){ + for(j = 0; j < two; j++){ + tmp_arr[i*two + j] = 0; + } + } + i = 0; + for(vec_int_itr = cellsOnEdge.begin(); vec_int_itr != cellsOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*two + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "cellsOnEdge", + NC_INT, "cells adj. to each edge", {"nEdges", "TWO"}); + + ncutil::put_var(outputFilename, "cellsOnEdge", &tmp_arr[0]); + + // Build VOE array + i = 0; + for(vec_int_itr = verticesOnEdge.begin(); vec_int_itr != verticesOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*two + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "verticesOnEdge", + NC_INT, "vertices on each edge", {"nEdges", "TWO"}); + + ncutil::put_var(outputFilename, "verticesOnEdge", &tmp_arr[0]); + + delete[] tmp_arr; + + // Build and write nEoe array + tmp_arr = new int[nEdges]; + i = 0; + for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ + tmp_arr[i] = (*vec_int_itr).size(); + i++; + } + + ncutil::def_var(outputFilename, "nEdgesOnEdge", + NC_INT, "number of edges on each edge", {"nEdges"}); + + ncutil::put_var(outputFilename, "nEdgesOnEdge", &tmp_arr[0]); + + delete[] tmp_arr; + + cellsOnEdge.clear(); +// verticesOnEdge.clear(); // Needed for Initial conditions. + edgesOnEdge.clear(); + + return 0; +}/*}}}*/ +int outputVertexConnectivity( const string outputFilename) {/*{{{*/ + /***************************************************************** + * + * This function writes all of the *OnVertex arrays. Including + * cellsOnVertex + * edgesOnVertex + * + * ***************************************************************/ + + int nVertices = vertices.size(); + int vertexDegree = vertex_degree; + int i, j; + + int *tmp_arr; + + // Build and write COV array + tmp_arr = new int[nVertices*vertexDegree]; + + for(i = 0; i < nVertices; i++){ + for(j = 0; j < vertexDegree; j++){ + tmp_arr[i*vertexDegree + j] = 0; + } + } + + i = 0; + for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "cellsOnVertex", + NC_INT, "vertices adj. to each vertex", {"nVertices", "vertexDegree"}); + + ncutil::put_var(outputFilename, "cellsOnVertex", &tmp_arr[0]); + + // Build and write EOV array + for(i = 0; i < nVertices; i++){ + for(j = 0; j < vertexDegree; j++){ + tmp_arr[i*vertexDegree + j] = 0; + } + } + i = 0; + for(vec_int_itr = edgesOnVertex.begin(); vec_int_itr != edgesOnVertex.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnVertex", + NC_INT, "edges adj. to each vertex", {"nVertices", "vertexDegree"}); + + ncutil::put_var(outputFilename, "edgesOnVertex", &tmp_arr[0]); + + delete[] tmp_arr; + + // Build and write bdryVert array + tmp_arr = new int[nVertices]; + + i = 0; + for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ + if((*vec_int_itr).size() == vertexDegree){ + tmp_arr[i] = 0; + } else { + tmp_arr[i] = 1; + } + i++; + } + + ncutil::def_var(outputFilename, "boundaryVertex", + NC_INT, "non-zero for each vertex on mesh boundary", {"nVertices"}); + + ncutil::put_var(outputFilename, "boundaryVertex", &tmp_arr[0]); + + delete[] tmp_arr; + + cellsOnVertex.clear(); + edgesOnVertex.clear(); + + return 0; +}/*}}}*/ +int outputCellParameters( const string outputFilename) {/*{{{*/ + /********************************************************* + * + * This function writes all cell parameters, including + * areaCell + * + * *******************************************************/ + + int nCells = cells.size(); + int i, j; + + if(spherical){ + for(i = 0; i < nCells; i ++){ + areaCell[i] = areaCell[i] * sphereRadius * sphereRadius; + } + } + + ncutil::def_var(outputFilename, "areaCell", + NC_DOUBLE, "surface areas of cells", {"nCells"}); + + ncutil::put_var(outputFilename, "areaCell", &areaCell[0]); + + areaCell.clear(); + + return 0; +}/*}}}*/ +int outputVertexParameters( const string outputFilename) {/*{{{*/ + /********************************************************* + * + * This function writes all vertex parameters, including + * areaTriangle + * kiteAreasOnVertex + * + * *******************************************************/ + + int nVertices = vertices.size(); + int vertexDegree = vertex_degree; + int i, j; + int count; + + double *tmp_arr; + + if(spherical){ + for(i = 0; i < nVertices; i++){ + areaTriangle[i] = areaTriangle[i] * sphereRadius * sphereRadius; + } + } + + ncutil::def_var(outputFilename, "areaTriangle", + NC_DOUBLE, "surface areas of dual cells", {"nVertices"}); + + ncutil::put_var(outputFilename, "areaTriangle", &areaTriangle[0]); + + // Build and write kiteAreasOnVertex + // TODO: Fix kite area for quads? + tmp_arr = new double[nVertices*vertexDegree]; + for(i = 0; i < nVertices*vertexDegree; i++){ + tmp_arr[i] = 0.0; + } + i = 0; + count = 0; + for(vec_dbl_itr = kiteAreasOnVertex.begin(); vec_dbl_itr != kiteAreasOnVertex.end(); ++vec_dbl_itr){ + j = 0; + for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ + if(!spherical){ + tmp_arr[i*vertexDegree + j] = (*dbl_itr); + } else { + tmp_arr[i*vertexDegree + j] = (*dbl_itr) * sphereRadius * sphereRadius; + } + j++; + count++; + } + i++; + } + + ncutil::def_var(outputFilename, "kiteAreasOnVertex", + NC_DOUBLE, + "surface areas of overlap between cells and dual cells", {"nVertices", "vertexDegree"}); + + ncutil::put_var(outputFilename, "kiteAreasOnVertex", &tmp_arr[0]); + + delete[] tmp_arr; + + kiteAreasOnVertex.clear(); + + return 0; +}/*}}}*/ +int outputEdgeParameters( const string outputFilename) {/*{{{*/ + /********************************************************* + * + * This function writes all grid parameters, including + * angleEdge + * dcEdge + * dvEdge + * weightsOnEdge + * + * *******************************************************/ + + int nEdges = edges.size(); + int maxEdges2 = maxEdges * 2; + int i, j; + + double *tmp_arr; + + if(spherical){ + for(i = 0; i < nEdges; i++){ + dcEdge[i] = dcEdge[i] * sphereRadius; + dvEdge[i] = dvEdge[i] * sphereRadius; + } + } + + ncutil::def_var(outputFilename, "angleEdge", + NC_DOUBLE, "angle to edges", {"nEdges"}) ; + + ncutil::put_var(outputFilename, "angleEdge", &angleEdge[0]); + + ncutil::def_var(outputFilename, "dcEdge", + NC_DOUBLE, "length of arc between centres", {"nEdges"}); + ncutil::def_var(outputFilename, "dvEdge", + NC_DOUBLE, "length of arc between vertices", {"nEdges"}); + + ncutil::put_var(outputFilename, "dcEdge", &dcEdge[0]) ; + ncutil::put_var(outputFilename, "dvEdge", &dvEdge[0]) ; + + //Build and write weightsOnEdge + tmp_arr = new double[nEdges*maxEdges2]; + + i = 0; + for(vec_dbl_itr = weightsOnEdge.begin(); vec_dbl_itr != weightsOnEdge.end(); ++vec_dbl_itr){ + for(j = 0; j < maxEdges2; j++){ + tmp_arr[i*maxEdges2 + j] = 0.0; + } + + j = 0; + for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ + tmp_arr[i*maxEdges2 + j] = (*dbl_itr); + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "weightsOnEdge", + NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); + + ncutil::put_var(outputFilename, "weightsOnEdge", &tmp_arr[0]); + + delete[] tmp_arr; + + angleEdge.clear(); + dcEdge.clear(); + dvEdge.clear(); + weightsOnEdge.clear(); + + return 0; +}/*}}}*/ +int outputMeshDensity( const string outputFilename) {/*{{{*/ + /*************************************************************************** + * + * This function writes the meshDensity variable. Read in from the file SaveDensity + * + * *************************************************************************/ + + ncutil::def_var(outputFilename, "meshDensity", + NC_DOUBLE, "mesh density distribution", {"nCells"}); + + ncutil::put_var(outputFilename, "meshDensity", &meshDensity[0]); + + return 0; +}/*}}}*/ +int outputMeshQualities( const string outputFilename) {/*{{{*/ + /*************************************************************************** + * + * This function writes the mesh quality variables. + * - cellQuality + * - gridSpacing + * - triangleQuality + * - triangleAnalgeQuality + * - obtuseTriangle + * + * *************************************************************************/ + + ncutil::def_var(outputFilename, "cellQuality", + NC_DOUBLE, "quality of mesh cells", {"nCells"}); + + ncutil::put_var(outputFilename, "cellQuality", &cellQuality[0]); + + ncutil::def_var(outputFilename, "gridSpacing", + NC_DOUBLE, "grid spacing distribution", {"nCells"}); + + ncutil::put_var(outputFilename, "gridSpacing", &cellQuality[0]); + + ncutil::def_var(outputFilename, "triangleQuality", + NC_DOUBLE, "quality of mesh dual cells", {"nVertices"}); + + ncutil::put_var(outputFilename, "triangleQuality", &triangleQuality[0]); + + ncutil::def_var(outputFilename, "triangleAngleQuality", + NC_DOUBLE, "quality of mesh dual cells", {"nVertices"}); + + ncutil::put_var(outputFilename, + "triangleAngleQuality", &triangleAngleQuality [0]); + + ncutil::def_var(outputFilename, "obtuseTriangle", + NC_INT, + "non-zero for any dual cell containing obtuse angles", {"nVertices"}); + + ncutil::put_var(outputFilename, "obtuseTriangle", &obtuseTriangle[0]); + + cellQuality.clear(); + gridSpacing.clear(); + triangleQuality.clear(); + triangleAngleQuality.clear(); + obtuseTriangle.clear(); + + return 0; +}/*}}}*/ +int writeGraphFile(const string outputFilename){/*{{{*/ + ofstream graph(outputFilename.c_str()); + + int edgeCount = 0; + + for (int iCell = 0; iCell < nCells; iCell++){ + for ( int j = 0; j < cellsOnCell.at(iCell).size(); j++){ + int coc = cellsOnCell.at(iCell).at(j); + + if ( coc >= 0 && coc < nCells ) { + edgeCount++; + } + } + } + + edgeCount = edgeCount / 2; + graph << cells.size() << " " << edgeCount << endl; + + for(int i = 0; i < cellsOnCell.size(); i++){ + for(int j = 0; j < cellsOnCell.at(i).size(); j++){ + if ( cellsOnCell.at(i).at(j) >= 0 ) { + graph << cellsOnCell.at(i).at(j)+1 << " "; + } + } + graph << endl; + } + + graph.close(); + + return 0; +}/*}}}*/ +/*}}}*/ + +string gen_random(const int len) {/*{{{*/ + static const char alphanum[] = + "0123456789" +// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + string rand_str = ""; + + for (int i = 0; i < len; ++i) { + rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; + } + + return rand_str; +}/*}}}*/ + diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h b/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h new file mode 100755 index 000000000..46095cc9e --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h @@ -0,0 +1,494 @@ + + /* + -------------------------------------------------------- + * NCUTIL: simple interface to the NetCDF library. + -------------------------------------------------------- + * + * Last updated: 20 May, 2022 + * + * borrowed from the jigsaw library: + * https://github.com/dengwirda/jigsaw + * + -------------------------------------------------------- + */ + +# include +# include +# include + +# include + +# pragma once + +# ifndef __NCUTIL__ +# define __NCUTIL__ + + namespace ncutil { + +# define __const_ptr(T) T const *__restrict +# define __write_ptr(T) T *__restrict + +# define __inline_call inline + + /* + -------------------------------------------------------- + * GET/PUT-DIM: handlers for NetCDF dimensions + -------------------------------------------------------- + */ + + __inline_call void def_dim ( + std::string const&_file, // name of file + std::string const&_name, // name of dimension + size_t _dlen // length of dimension + ) + { + int _retv, _ncid, _dtag; + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for write: " + std::to_string(_retv)); + + if ((_retv = nc_def_dim( + _ncid, _name.c_str(), _dlen, &_dtag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting dimension " + + _name + " :" + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + __inline_call void get_dim ( + std::string const&_file, // name of file + std::string const&_name, // name of dimension + size_t &_dlen // length of dimension + ) + { + int _retv, _ncid, _dtag; + + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if ((_retv = nc_inq_dimid(_ncid, _name.c_str(), &_dtag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting dimension " + + _name + " : " + std::to_string(_retv)); + } + + if ((_retv = nc_inq_dimlen(_ncid, _dtag, &_dlen))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting dimension " + + _name + " : " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + /* + -------------------------------------------------------- + * GET/PUT-ATT: handlers for NetCDF attributes + -------------------------------------------------------- + */ + + template < + typename data_type + > + __inline_call void get_att ( + std::string const&_file, // name of file + std::string const&_name, // name of attribute + __write_ptr(data_type) _vals, // ptr to attr. value array + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if ((_retv = nc_get_att(_ncid, + _vtag, _name.c_str (), (void *) _vals))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + __inline_call void get_str ( + std::string const&_file, // name of file + std::string const&_name, // name of attribute + std::string &_vals, // attr. string + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + size_t _slen; + + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if ((_retv = nc_inq_attlen( + _ncid, _vtag, _name.c_str(), &_slen))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + + _vals.resize(_slen + 1) ; // note: init to null + if ((_retv = nc_get_att_text( + _ncid, _vtag, _name.c_str(), &_vals[0]))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + template < + typename data_type + > + __inline_call void put_att ( + std::string const&_file, // name of file + std::string const&_name, // name of attribute + nc_type _type, // NetCDF data-type + data_type const&_vals, // single attr. value + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for write: " + std::to_string(_retv)); + + if ((_retv = nc_put_att( + _ncid, _vtag, + _name.c_str(), _type, +1, (void *) &_vals))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting attribute " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + __inline_call void put_str ( + std::string const&_file, // name of file + std::string const&_name, // name of attribute + std::string const&_vals, // attr. string + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if ((_retv = nc_put_att_text(_ncid, _vtag, + _name.c_str (), _vals.size(), _vals.c_str()))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + template < + typename data_type + > + __inline_call void put_att ( + std::string const&_file, // name of file + std::string const&_name, // name of attribute + nc_type _type, // NetCDF data-type + size_t _nval, // length of attr. value array + __const_ptr(data_type) _vals, // ptr to att. value array + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for write: " + std::to_string(_retv)); + + if ((_retv = nc_put_att( + _ncid, _vtag , + _name.c_str(), _type, _nval, (void *) _vals))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting attribute " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + /* + -------------------------------------------------------- + * GET/PUT-VAR: handlers for NetCDF variables + -------------------------------------------------------- + */ + + __inline_call void def_var ( + std::string const&_file, // name of file + std::string const&_name, // name of variable + nc_type _type, // NetCDF data-type + std::string const&_long, // NetCDF long_name + std::initializer_list _dims // dim. name list + ) + { + int _retv, _ncid, _vtag, _dtag[256]; + int _idim = +0; + + if (_dims.size() > +256) + throw std::invalid_argument( + "Error putting variable " + + _name + " : too many dimensions!"); + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for write: " + std::to_string(_retv)); + + for (auto _elem : _dims) + { + if ((_retv = nc_inq_dimid( + _ncid, _elem.c_str(), &_dtag[ _idim++ ]))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting variable " + + _name + " : " + std::to_string(_retv)); + } + } + + if ((_retv = nc_def_var( + _ncid, _name.c_str(), + _type, _dims.size (), _dtag, &_vtag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting variable " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_put_att_text(_ncid, _vtag, + "long_name", _long.size(), _long.c_str()))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting variable " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + template < + typename data_type + > + __inline_call void get_var ( + std::string const&_file, // name of file + std::string const&_name, // name of variable + __write_ptr(data_type) _vals // ptr to var. value array + ) + { + int _retv, _ncid, _vtag; + + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if ((_retv = nc_inq_varid(_ncid, _name.c_str(), &_vtag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting variable " + + _name + " : " + std::to_string(_retv)); + } + + if ((_retv = nc_get_var(_ncid, _vtag, (void *) _vals))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting variable " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + template < + typename data_type + > + __inline_call void put_var ( + std::string const&_file, // name of file + std::string const&_name, // name of variable + __const_ptr(data_type) _vals // ptr to var. value array + ) + { + int _retv, _ncid, _vtag; + + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for write: " + std::to_string(_retv)); + + if ((_retv = nc_inq_varid(_ncid, _name.c_str(), &_vtag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting variable " + + _name + " : " + std::to_string(_retv)); + } + + if ((_retv = nc_put_var(_ncid, _vtag, (void *) _vals))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error putting variable " + + _name + ": " + std::to_string(_retv)); + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + + /* + -------------------------------------------------------- + * GET/PUT-TAG: handlers for NetCDF ID tags. + -------------------------------------------------------- + */ + + int static const NC_VARIABLE = 100; + int static const NC_ATTRIBUTE = 101; + int static const NC_DIMENSION = 102; + + __inline_call void get_tag ( + std::string const&_file, // name of file + std::string const&_name, // name of dim./att./var. + int _kind, int &_itag, // kind and idtag of name + int _vtag = NC_GLOBAL // NetCDF varid; default global + ) + { + int _retv, _ncid; + + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) + throw std::invalid_argument( + "Can't open " + _file + + " for read.: " + std::to_string(_retv)); + + if (_kind == NC_DIMENSION) + { + if ((_retv = nc_inq_dimid( + _ncid, _name.c_str(), &_itag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + } + else + if (_kind == NC_VARIABLE) + { + if ((_retv = nc_inq_varid( + _ncid, _name.c_str(), &_itag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + } + else + if (_kind == NC_ATTRIBUTE) + { + if ((_retv = nc_inq_attid( + _ncid, _vtag, _name.c_str(), &_itag))) + { + nc_close(_ncid) ; + throw std::invalid_argument( + "Error getting attribute " + + _name + ": " + std::to_string(_retv)); + } + } + + if ((_retv = nc_close(_ncid))) + throw std::invalid_argument( + "Error handling " + + _file + " close: " + std::to_string(_retv)); + } + +# undef __const_ptr +# undef __write_ptr + +# undef __inline_call + + } + +# endif //__NCUTIL__ + + + diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h new file mode 100755 index 000000000..0c8100038 --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h @@ -0,0 +1,509 @@ +#include +#include +#include +#include + +class pnt {/*{{{*/ + public: + double x, y, z; + double lat, lon; + int idx; + bool positiveLonRange; + + pnt(double x_, double y_, double z_, int idx_) { + (*this).x = x_; + (*this).y = y_; + (*this).z = z_; + (*this).idx = idx_; + (*this).positiveLonRange = true; + (*this).buildLat(); + (*this).buildLon(); + } + + pnt(double x_, double y_, double z_) { + (*this).x = x_; + (*this).y = y_; + (*this).z = z_; + (*this).idx = 0; + (*this).positiveLonRange = true; + (*this).buildLat(); + (*this).buildLon(); + } + + pnt() { + (*this).x = 0.0; + (*this).y = 0.0; + (*this).z = 0.0; + (*this).idx = 0; + (*this).positiveLonRange = true; + (*this).lat = 0.0; + (*this).lon = 0.0; + } + + friend pnt operator*(const double d, const pnt &p); + friend std::ostream & operator<<(std::ostream &os, const pnt &p); + friend std::istream & operator>>(std::istream &is, pnt &p); + + pnt& operator=(const pnt &p){/*{{{*/ + x = p.x; + y = p.y; + z = p.z; + idx = p.idx; + (*this).buildLat(); + (*this).buildLon(); + return *this; + }/*}}}*/ + bool operator==(const pnt &p) const {/*{{{*/ + return (x == p.x) & (y == p.y) & (z == p.z); + }/*}}}*/ + pnt operator-(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = x-p.x; + y_ = y-p.y; + z_ = z-p.z; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator+(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = x+p.x; + y_ = y+p.y; + z_ = z+p.z; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator*(double d) const {/*{{{*/ + double x_, y_, z_; + x_ = x*d; + y_ = y*d; + z_ = z*d; + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator/(double d) const {/*{{{*/ + double x_, y_, z_; + + if(d == 0.0){ + std::cout << "pnt: operator/" << std::endl; + std::cout << (*this) << std::endl; + } + + assert(d != 0.0); + x_ = x/d; + y_ = y/d; + z_ = z/d; + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt& operator/=(double d){/*{{{*/ + if(d == 0.0){ + std::cout << "pnt: operator /=" << std::endl << (*this) << std::endl; + } + assert(d != 0.0); + x = x/d; + y = y/d; + z = z/d; + return *this; + }/*}}}*/ + pnt& operator+=(const pnt &p){/*{{{*/ + x += p.x; + y += p.y; + z += p.z; + return *this; + }/*}}}*/ + double operator[](int i) const {/*{{{*/ + if(i == 0){ + return x; + } else if(i == 1){ + return y; + } else { + return z; + } + }/*}}}*/ + void normalize(){/*{{{*/ + double norm; + + norm = x*x + y*y + z*z; + if(norm == 0){ + std::cout << "pnt: normalize" << std::endl; + std::cout << x << " " << y << " " << z << " " << idx << std::endl; + + assert(norm != 0); + } + norm = sqrt(norm); + + x = x/norm; + y = y/norm; + z = z/norm; + }/*}}}*/ + double dot(const pnt &p) const {/*{{{*/ + double junk; + junk = x*p.x+y*p.y+z*p.z; + + return junk; + }/*}}}*/ + double dotForAngle(const pnt &p) const {/*{{{*/ + double junk; + junk = x*p.x+y*p.y+z*p.z; + if(junk > 1.0){ + junk = 1.0; + } + + if(junk < -1.0){ + junk = -1.0; + } + return acos(junk); + }/*}}}*/ + pnt cross(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = y*p.z - p.y*z; + y_ = z*p.x - p.z*x; + z_ = x*p.y - p.x*y; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + void fixPeriodicity(const pnt &p, const double xRef, const double yRef){/*{{{*/ + /* The fixPeriodicity function fixes the periodicity of the current point relative + * to point p. It should only be used on a point in the x/y plane. + * xRef and yRef should be the extents of the non-periodic plane. + */ + + pnt dist_vec; + + dist_vec = (*this) - p; + + if(fabs(dist_vec.x) > xRef * 0.6){ +#ifdef _DEBUG + std::cout << " Fixing x periodicity " << endl; +#endif + (*this).x += -(dist_vec.x/fabs(dist_vec.x)) * xRef; + } + if(fabs(dist_vec.y) > yRef * 0.6){ +#ifdef _DEBUG + std::cout << " Fixing y periodicity " << endl; +#endif + (*this).y += -(dist_vec.y/fabs(dist_vec.y)) * yRef; + } + + }/*}}}*/ + double magnitude() const{/*{{{*/ + return sqrt(x*x + y*y + z*z); + }/*}}}*/ + double magnitude2() const {/*{{{*/ + return x*x + y*y + z*z; + }/*}}}*/ + void rotate(const pnt &vec, const double angle){/*{{{*/ + double x_, y_, z_; + double cos_t, sin_t; + + cos_t = cos(angle); + sin_t = sin(angle); + + x_ = (cos_t + vec.x*vec.x *(1.0-cos_t)) * x + +(vec.x*vec.y*(1.0-cos_t) - vec.z * sin_t) * y + +(vec.x*vec.z*(1.0-cos_t) + vec.y * sin_t) * z; + + y_ = (vec.y*vec.x*(1.0-cos_t) + vec.z*sin_t) * x + +(cos_t + vec.y*vec.x*(1.0 - cos_t)) * y + +(vec.y*vec.z*(1.0-cos_t) - vec.x*sin_t) * z; + + z_ = (vec.z*vec.x*(1.0-cos_t)-vec.y*sin_t)*x + +(vec.z*vec.y*(1.0-cos_t)-vec.x*sin_t)*y + +(cos_t + vec.x*vec.x*(1.0-cos_t))*z; + + x = x_; + y = y_; + z = z_; + }/*}}}*/ + void setPositiveLonRange(bool value){/*{{{*/ + (*this).positiveLonRange = value; + (*this).buildLon(); + }/*}}}*/ + double getLat() const {/*{{{*/ + return (*this).lat; + }/*}}}*/ + double getLon() const {/*{{{*/ + return (*this).lon; + }/*}}}*/ + void buildLat() {/*{{{*/ + double dl; + + dl = sqrt((*this).x*(*this).x + (*this).y*(*this).y + (*this).z*(*this).z); + (*this).lat = asin(z/dl); + }/*}}}*/ + void buildLon() {/*{{{*/ + double lon; + + lon = atan2(y, x); + + // If the prime meridian is the minimum, this means the degree + // range is 0-360, so we need to translate. + if ( (*this).positiveLonRange ) { + if(lon < 0.0) { + lon = 2.0*M_PI + lon; + } + } + + (*this).lon = lon; + }/*}}}*/ + double sphereDistance(const pnt &p) const {/*{{{*/ + double arg; + + arg = sqrt( powf(sin(0.5*((*this).getLat()-p.getLat())),2) + + cos(p.getLat())*cos((*this).getLat())*powf(sin(0.5*((*this).getLon()-p.getLon())),2)); + return 2.0*asin(arg); + + /* + pnt temp, cross; + double dot; + temp.x = x; + temp.y = y; + temp.z = z; + + cross = temp.cross(p); + dot = temp.dot(p); + + + + return atan2(cross.magnitude(),dot); + // */ + }/*}}}*/ + struct hasher {/*{{{*/ + size_t operator()(const pnt &p) const { + uint32_t hash; + size_t i, key[3] = { (size_t)p.x, (size_t)p.y, (size_t)p.z }; + for(hash = i = 0; i < sizeof(key); ++i) { + hash += ((uint8_t *)key)[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + };/*}}}*/ + struct idx_hasher {/*{{{*/ + size_t operator()(const pnt &p) const { + return (size_t)p.idx; + } + };/*}}}*/ +};/*}}}*/ + +inline pnt operator*(const double d, const pnt &p){/*{{{*/ + return pnt(d*p.x, d*p.y, d*p.z, 0); +}/*}}}*/ + +inline std::ostream & operator<<(std::ostream &os, const pnt &p){/*{{{*/ + os << '(' << p.x << ", " << p.y << ", " << p.z << ") idx=" << p.idx << ' '; + //os << p.x << " " << p.y << " " << p.z; + return os; +}/*}}}*/ +inline std::istream & operator>>(std::istream &is, pnt &p){/*{{{*/ + //is >> p.x >> p.y >> p.z >> p.idx; + is >> p.x >> p.y >> p.z; + return is; +}/*}}}*/ + +/* Geometric utility functions {{{ */ +pnt gcIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){/*{{{*/ + /* + * gcIntersect is intended to compute the intersection of two great circles. + * The great circles pass through the point sets c1-c2 and v1-v2. + */ + + /* + pnt n, m,c ; + double dot; + + + //n = c1 cross c2 + n = c1.cross(c2); + + //m = v1 cross v2 + m = v1.cross(v2); + + //c = n cross m + c = n.cross(m); + + dot = c1.dot(c); + + dot = dot/fabs(dot); + + c = c*dot; + + c.normalize(); + + return c; + // */ + + double n1, n2, n3; + double m1, m2, m3; + double xc, yc, zc; + double dot; + pnt c; + + n1 = (c1.y * c2.z - c2.y * c1.z); + n2 = -(c1.x * c2.z - c2.x * c1.z); + n3 = (c1.x * c2.y - c2.x * c1.y); + + m1 = (v1.y * v2.z - v2.y * v1.z); + m2 = -(v1.x * v2.z - v2.x * v1.z); + m3 = (v1.x * v2.y - v2.x * v1.y); + + xc = (n2 * m3 - n3 * m2); + yc = -(n1 * m3 - n3 * m1); + zc = (n1 * m2 - n2 * m1); + + dot = c1.x*xc + c1.y*yc + c1.z*zc; + + if (dot < 0.0) { + xc = -xc; + yc = -yc; + zc = -zc; + } + + c.x = xc; + c.y = yc; + c.z = zc; + + c.normalize(); + return c; +}/*}}}*/ +pnt planarIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){/*{{{*/ + /* + * planarIntersect is intended to compute the point of intersection + * of the lines c1-c2 and v1-v2 in a plane. + */ + double alpha_numerator, beta_numerator, denom; + double alpha, beta; + pnt c; + + alpha_numerator = (v2.x - v1.x) * (c1.y - v2.y) - (v2.y - v1.y) * (c1.x - v2.x); +// beta_numerator = (c1.x - v2.x) * (c2.y - c1.y) - (c1.y - v2.y) * (c2.x - c1.x); + denom = (c2.x - c1.x) * (v2.y - v1.y) - (c2.y - c1.y) * (v2.x - v1.x); + + alpha = alpha_numerator / denom; +// beta = beta_numerator / demon; + + c = alpha * (v2 - v1) + v1; + + return c; +}/*}}}*/ + +double planarTriangleArea(const pnt &A, const pnt &B, const pnt &C){/*{{{*/ + /* + * planarTriangleArea uses Heron's formula to compute the area of a triangle in a plane. + */ + pnt ab, bc, ca; + double s, a, b, c, e; + + ab = B - A; + bc = C - B; + ca = A - C; + + // Get side lengths + a = ab.magnitude(); + b = bc.magnitude(); + c = ca.magnitude(); + + // Semi-perimeter of TRI(ABC) + s = (a + b + c) * 0.5; + + return sqrt(s * (s - a) * (s - b) * (s - c)); +}/*}}}*/ +double sphericalTriangleArea(const pnt &A, const pnt &B, const pnt &C){/*{{{*/ + /* + * sphericalTriangleArea uses the spherical analog of Heron's formula to compute + * the area of a triangle on the surface of a sphere. + * + */ + double tanqe, s, a, b, c, e; + + a = A.sphereDistance(B); + b = B.sphereDistance(C); + c = C.sphereDistance(A); + s = 0.5*(a+b+c); + + tanqe = sqrt(tan(0.5*s)*tan(0.5*(s-a))*tan(0.5*(s-b))*tan(0.5*(s-c))); + e = 4.*atan(tanqe); + + return e; +}/*}}}*/ + +double planeAngle(const pnt &A, const pnt &B, const pnt &C, const pnt &n){/*{{{*/ + /* + * planeAngle computes the angles between the vectors AB and AC in a plane defined with + * the normal vector n + */ + + /* + pnt ab; + pnt ac; + pnt cross; + double cos_angle; + + ab = B - A; + ac = C - A; + cross = ab.cross(ac); + cos_angle = ab.dot(ac)/(ab.magnitude() * ac.magnitude()); + + cos_angle = std::min(std::max(cos_angle,-1.0),1.0); + + if(cross.x*n.x + cross.y*n.y + cross.z*n.z >= 0){ + return acos(cos_angle); + } else { + return -acos(cos_angle); + } + // */ + + double ABx, ABy, ABz, mAB; + double ACx, ACy, ACz, mAC; + double Dx, Dy, Dz; + double cos_angle; + + ABx = B.x - A.x; + ABy = B.y - A.y; + ABz = B.z - A.z; + mAB = sqrt(ABx*ABx + ABy*ABy + ABz*ABz); + + ACx = C.x - A.x; + ACy = C.y - A.y; + ACz = C.z - A.z; + mAC = sqrt(ACx*ACx + ACy*ACy + ACz*ACz); + + + Dx = (ABy * ACz) - (ABz * ACy); + Dy = -((ABx * ACz) - (ABz * ACx)); + Dz = (ABx * ACy) - (ABy * ACx); + + cos_angle = (ABx*ACx + ABy*ACy + ABz*ACz) / (mAB * mAC); + + if (cos_angle < -1.0) { + cos_angle = -1.0; + } else if (cos_angle > 1.0) { + cos_angle = 1.0; + } + + if ((Dx*n.x + Dy*n.y + Dz*n.z) >= 0.0) { + return acos(cos_angle); + } else { + return -acos(cos_angle); + } +}/*}}}*/ +pnt pntFromLatLon(const double &lat, const double &lon){/*{{{*/ + /* + * pntFromLatLon constructs a point location in 3-Space + * from an initial latitude and longitude location. + */ + pnt temp; + temp.x = cos(lon) * cos(lat); + temp.y = sin(lon) * cos(lat); + temp.z = sin(lat); + temp.normalize(); + temp.buildLat(); + temp.buildLon(); + return temp; +}/*}}}*/ +/*}}}*/ diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/string_utils.h b/mesh_tools/mesh_conversion_tools_netcdf_c/string_utils.h new file mode 100755 index 000000000..39592936b --- /dev/null +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/string_utils.h @@ -0,0 +1,104 @@ + +# include + +# pragma once + +# ifndef __STRING_UTILS__ +# define __STRING_UTILS__ + + /* + -------------------------------------------------------- + * SEPARATOR: return the OS-specific path separator + -------------------------------------------------------- + */ + + inline char separator ( + ) + { +# ifdef _WIN32 + return '\\'; +# else + return '/'; +# endif + } + + /* + -------------------------------------------------------- + * PATH-JOIN: joint two path names with a separator + -------------------------------------------------------- + */ + + std::string path_join ( + std::string const& _root, + std::string const& _stem + ) + { + if(!_root.empty() && !_stem.empty() && + _root. back() != separator() && + _stem.front() != separator()) + { + return _root + separator() + _stem; + } + else + { + return _root + _stem; + } + } + + /* + -------------------------------------------------------- + * FILE-PART: split a file name into path/name.fext + -------------------------------------------------------- + */ + + void file_part ( + std::string const& _fsrc, + std::string & _path, + std::string & _name, + std::string & _fext + ) + { + typename std::string::size_type + _spos = _fsrc.find_last_of("\\/"); + + typename std::string::size_type + _dpos = _fsrc.find_last_of("." ); + + typename std::string::const_iterator + _pos0, _pos1, _pos2, + _pos3, _pos4, _pos5; + + if (_spos != std::string::npos ) + { + _pos0 = _fsrc.begin(); + _pos1 = _fsrc.begin()+_spos-0; + _pos2 = _fsrc.begin()+_spos+1; + } + else + { + _pos0 = _fsrc.begin(); + _pos1 = _fsrc.begin(); + _pos2 = _fsrc.begin(); + } + + if (_dpos != std::string::npos && + (_spos == std::string::npos || + _dpos >= _spos) ) + { + _pos3 = _fsrc.begin()+_dpos-0; + _pos4 = _fsrc.begin()+_dpos+1; + _pos5 = _fsrc.end (); + } + else + { + _pos3 = _fsrc.end (); + _pos4 = _fsrc.end (); + _pos5 = _fsrc.end (); + } + + _path = std::string(_pos0, _pos1); + _name = std::string(_pos2, _pos3); + _fext = std::string(_pos4, _pos5); + } + +# endif //__STRING_UTILS__ From 2956e63ee3f99beff06ee89393bd51ab5b0c8f37 Mon Sep 17 00:00:00 2001 From: dengwirda Date: Thu, 6 Jul 2023 10:25:59 -0600 Subject: [PATCH 011/169] Add bandwidth-minimising mesh sorting routine --- .../mpas_tools/mesh/creation/sort_mesh.py | 187 ++++++++++++++++++ conda_package/setup.py | 1 + 2 files changed, 188 insertions(+) create mode 100644 conda_package/mpas_tools/mesh/creation/sort_mesh.py diff --git a/conda_package/mpas_tools/mesh/creation/sort_mesh.py b/conda_package/mpas_tools/mesh/creation/sort_mesh.py new file mode 100644 index 000000000..f55375a5c --- /dev/null +++ b/conda_package/mpas_tools/mesh/creation/sort_mesh.py @@ -0,0 +1,187 @@ + +import numpy as np +import os +import xarray +import argparse +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import reverse_cuthill_mckee + + +def sort_fwd(data, fwd): + vals = data.values + vals = vals[fwd - 1] + return vals + + +def sort_rev(data, rev): + vals = data.values + mask = vals > 0 + vals[mask] = rev[vals[mask] - 1] + return vals + + +def sort_mesh(mesh): + """ + SORT-MESH: sort cells, edges and duals in the mesh + to improve cache-locality. + + """ + # Authors: Darren Engwirda + + ncells = mesh.dims["nCells"] + nedges = mesh.dims["nEdges"] + nduals = mesh.dims["nVertices"] + + cell_fwd = np.arange(0, ncells) + 1 + cell_rev = np.arange(0, ncells) + 1 + edge_fwd = np.arange(0, nedges) + 1 + edge_rev = np.arange(0, nedges) + 1 + dual_fwd = np.arange(0, nduals) + 1 + dual_rev = np.arange(0, nduals) + 1 + + # sort cells via RCM ordering of adjacency matrix + + cell_fwd = reverse_cuthill_mckee(cell_del2(mesh)) + 1 + + cell_rev = np.zeros(ncells, dtype=np.int32) + cell_rev[cell_fwd - 1] = np.arange(ncells) + 1 + + mesh["cellsOnCell"][:] = \ + sort_rev(mesh["cellsOnCell"], cell_rev) + mesh["cellsOnEdge"][:] = \ + sort_rev(mesh["cellsOnEdge"], cell_rev) + mesh["cellsOnVertex"][:] = \ + sort_rev(mesh["cellsOnVertex"], cell_rev) + + for var in mesh.keys(): + dims = mesh.variables[var].dims + if ("nCells" in dims): + mesh[var][:] = sort_fwd(mesh[var], cell_fwd) + + mesh["indexToCellID"][:] = np.arange(ncells) + 1 + + # sort duals via pseudo-linear cell-wise ordering + + dual_fwd = np.ravel(mesh["verticesOnCell"].values) + dual_fwd = dual_fwd[dual_fwd > 0] + + __, imap = np.unique(dual_fwd, return_index=True) + + dual_fwd = dual_fwd[np.sort(imap)] + + dual_rev = np.zeros(nduals, dtype=np.int32) + dual_rev[dual_fwd - 1] = np.arange(nduals) + 1 + + mesh["verticesOnCell"][:] = \ + sort_rev(mesh["verticesOnCell"], dual_rev) + + mesh["verticesOnEdge"][:] = \ + sort_rev(mesh["verticesOnEdge"], dual_rev) + + for var in mesh.keys(): + dims = mesh.variables[var].dims + if ("nVertices" in dims): + mesh[var][:] = sort_fwd(mesh[var], dual_fwd) + + mesh["indexToVertexID"][:] = np.arange(nduals) + 1 + + # sort edges via pseudo-linear cell-wise ordering + + edge_fwd = np.ravel(mesh["edgesOnCell"].values) + edge_fwd = edge_fwd[edge_fwd > 0] + + __, imap = np.unique(edge_fwd, return_index=True) + + edge_fwd = edge_fwd[np.sort(imap)] + + edge_rev = np.zeros(nedges, dtype=np.int32) + edge_rev[edge_fwd - 1] = np.arange(nedges) + 1 + + mesh["edgesOnCell"][:] = \ + sort_rev(mesh["edgesOnCell"], edge_rev) + + mesh["edgesOnEdge"][:] = \ + sort_rev(mesh["edgesOnEdge"], edge_rev) + + mesh["edgesOnVertex"][:] = \ + sort_rev(mesh["edgesOnVertex"], edge_rev) + + for var in mesh.keys(): + dims = mesh.variables[var].dims + if ("nEdges" in dims): + mesh[var][:] = sort_fwd(mesh[var], edge_fwd) + + mesh["indexToEdgeID"][:] = np.arange(nedges) + 1 + + return mesh + + +def cell_del2(mesh): + """ + CELL-DEL2: form cell-to-cell sparse adjacency graph + + """ + xvec = np.array([], dtype=np.int8) + ivec = np.array([], dtype=np.int32) + jvec = np.array([], dtype=np.int32) + + topolOnCell = mesh["nEdgesOnCell"].values + cellsOnCell = mesh["cellsOnCell"].values + + for edge in range(np.max(topolOnCell)): + + # cell-to-cell pairs, if edges exist + mask = topolOnCell > edge + idx_self = np.argwhere(mask).ravel() + idx_next = cellsOnCell[mask, edge] - 1 + + # cell-to-cell pairs, if cells exist + mask = idx_next >= 0 + idx_self = idx_self[mask] + idx_next = idx_next[mask] + + # dummy matrix values, just topol. needed + val_edge = np.ones(idx_next.size, dtype=np.int8) + + ivec = np.hstack((ivec, idx_self)) + jvec = np.hstack((jvec, idx_next)) + xvec = np.hstack((xvec, val_edge)) + + return csr_matrix((xvec, (ivec, jvec))) + + +if (__name__ == "__main__"): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument( + "--mesh-file", dest="mesh_file", type=str, + required=True, help="Path+name to unsorted mesh file.") + + parser.add_argument( + "--sort-file", dest="sort_file", type=str, + required=True, help="Path+name to sorted output file.") + + args = parser.parse_args() + + mesh = xarray.open_dataset(args.mesh_file) + + sort_mesh(mesh) + + with open(os.path.join(os.path.dirname( + args.sort_file), "graph.info"), "w") as fptr: + cellsOnCell = mesh["cellsOnCell"].values + + ncells = mesh.dims["nCells"] + nedges = np.count_nonzero(cellsOnCell) // 2 + + fptr.write("{} {}\n".format(ncells, nedges)) + for cell in range(ncells): + data = cellsOnCell[cell, :] + data = data[data > 0] + for item in data: + fptr.write("{} ".format(item)) + fptr.write("\n") + + mesh.to_netcdf(args.sort_file, format="NETCDF4") diff --git a/conda_package/setup.py b/conda_package/setup.py index 82996002a..5506aadcb 100755 --- a/conda_package/setup.py +++ b/conda_package/setup.py @@ -89,6 +89,7 @@ 'mpas_to_triangle = mpas_tools.mesh.creation.mpas_to_triangle:main', 'triangle_to_netcdf = mpas_tools.mesh.creation.triangle_to_netcdf:main', 'jigsaw_to_netcdf = mpas_tools.mesh.creation.jigsaw_to_netcdf:main', + 'sort_mesh = mpas_tools.mesh.creation.sort_mesh:main', 'scrip_from_mpas = mpas_tools.scrip.from_mpas:main', 'compute_mpas_region_masks = mpas_tools.mesh.mask:entry_point_compute_mpas_region_masks', 'compute_mpas_transect_masks = mpas_tools.mesh.mask:entry_point_compute_mpas_transect_masks', From b34e58ed8cb7ef1a643c9b2d96d4d4adf188e623 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 18 Jul 2023 14:31:00 -0700 Subject: [PATCH 012/169] Deal with infinities in extrapolation Ensure that very large or infinite values are removed during extrapolation. --- landice/mesh_tools_li/extrapolate_variable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py index d72f83fe2..e5c883f35 100644 --- a/landice/mesh_tools_li/extrapolate_variable.py +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -29,6 +29,7 @@ options, args = parser.parse_args() dataset = Dataset(options.nc_file, 'r+') +dataset.set_auto_mask(False) var_name = options.var_name varValue = dataset.variables[var_name][0, :] extrapolation = options.extrapolation @@ -45,7 +46,7 @@ # Define region of good data to extrapolate from. Different methods for different variables if var_name in ["effectivePressure", "beta", "muFriction"]: groundedMask = (thickness > (-1028.0 / 910.0 * bed)) - keepCellMask = np.copy(groundedMask) + keepCellMask = np.copy(groundedMask) * np.isfinite(varValue) extrapolation == "min" for iCell in range(nCells): @@ -55,6 +56,7 @@ keepCellMask[iCell] = 1 continue keepCellMask *= (varValue > 0) # ensure zero muFriction does not get extrapolated + keepCellMask *= (varValue < 1.e12) # get rid of ridiculous values elif var_name in ["floatingBasalMassBal"]: floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) keepCellMask = floatingMask * (varValue != 0.0) From 58c00dd8b45d829df92fbb3e8c7e19011ba542e2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 27 Jun 2023 11:17:23 +0200 Subject: [PATCH 013/169] Remove trailing whitespace; change tabs to spaces --- .../mesh_conversion_tools_netcdf_c/README | 226 +- .../mesh_conversion_tools_netcdf_c/edge.h | 48 +- .../json/json-forwards.h | 14 +- .../json/json.h | 20 +- .../jsoncpp.cpp | 20 +- .../mpas_cell_culler.cpp | 2012 +++---- .../mpas_mesh_converter.cpp | 4984 ++++++++--------- .../netcdf_utils.h | 88 +- .../mesh_conversion_tools_netcdf_c/pnt.h | 894 +-- 9 files changed, 4153 insertions(+), 4153 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/README b/mesh_tools/mesh_conversion_tools_netcdf_c/README index 5bfb93cd6..ad56d073c 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/README +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/README @@ -3,130 +3,130 @@ Readme for mpas_mesh_converter.cpp and mpas_cell_culler.cpp Author: Doug Jacobsen Purpose: - mpas_mesh_converter.cpp is a piece of software designed create an MPAS mesh. - As input, this software takes the locations of MPAS cell centers, and cell - vertices, along with the connectivity array cellsOnVertex. If provided, it - will also migrate data from the meshDensity field, if it's not present it - will write 1.0 for every cell. - - mpas_cell_culler.cpp is a piece of software designed remove - cells/edge/vertices from an MPAS mesh. As input, this software takes a - valid MPAS mesh with one additional field "cullCell". This new field should - be nCells integers. A 1 means the cell should be kept, and a 0 means the - cell should be removed. - - mpas_mask_creator.cpp is a piece of software designed to create cell masks - from region definitions. Region definitions are defined in geojson files, - and can be created using the tools contained within the repository located - at: - https://github.com/MPAS-Dev/geometric_features - Masks have a value of 0 or 1, and are integers. - + mpas_mesh_converter.cpp is a piece of software designed create an MPAS mesh. + As input, this software takes the locations of MPAS cell centers, and cell + vertices, along with the connectivity array cellsOnVertex. If provided, it + will also migrate data from the meshDensity field, if it's not present it + will write 1.0 for every cell. + + mpas_cell_culler.cpp is a piece of software designed remove + cells/edge/vertices from an MPAS mesh. As input, this software takes a + valid MPAS mesh with one additional field "cullCell". This new field should + be nCells integers. A 1 means the cell should be kept, and a 0 means the + cell should be removed. + + mpas_mask_creator.cpp is a piece of software designed to create cell masks + from region definitions. Region definitions are defined in geojson files, + and can be created using the tools contained within the repository located + at: + https://github.com/MPAS-Dev/geometric_features + Masks have a value of 0 or 1, and are integers. + Requirements: - mpas_mesh_converter.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. - It also requires the tr1 headers for c++, specifically unordered_set. - It has been tested using g++ version 4.8.1 + mpas_mesh_converter.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It also requires the tr1 headers for c++, specifically unordered_set. + It has been tested using g++ version 4.8.1 - mpas_cell_culler.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. - It has been tested using g++ version 4.8.1 + mpas_cell_culler.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It has been tested using g++ version 4.8.1 - mpas_mask_creator.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. - It has been tested using g++ version 4.8.1 + mpas_mask_creator.cpp requires the c++ netcdf libraries to be able to read/write NetCDF files. + It has been tested using g++ version 4.8.1 Usage of mpas_mesh_converter.cpp: - ./MpasMeshConverter.x [input_name] [output_name] - - input_name: - The input_name should be the name of a NetCDF file containing the following information. - Dimensions: - nCells - The number of cells described in the file. - nVertices - The number of vertices described in the file. - vertexDegree - The maximum number of cells that surround each vertex. - Variables: - xCell -- The x location of every cell center. - yCell -- The y location of every cell center. - zCell -- The z location of every cell center. - xVertex -- The x location of every vertex (cell corner). - yVertex -- The y location of every vertex (cell corner). - zVertex -- The z location of every vertex (cell corner). - cellsOnVertex -- A list of cell indices that border each vertex (Dimensioned vertexDegree * nVertices). - This list can contain -1 values if the dual cell is not complete (e.g. a line rather than a triangle). - meshDensity -- (Optional) The value of the generating density function at each cell center. - If this variable is ommitted, it is filled with 1.0 on output. - Global Attributes: - on_a_sphere -- Should be "YES" if the cells/vertices are defined on a sphere and "NO" if they are defined in a plane. - history -- (Optional) Should be defined to a string describing how the input file was generated. - mesh_id -- (Optional) Should be a 40 character string (Alpha-Numeric) that can be used to identify the mesh. - If ommitted, a random string will be placed in the output file. - output_name: - The output_name should be the name of the NetCDF file that will be generated. It will be a valid MPAS mesh. - No initial conditions are placed in this file. If input_name is specified, output_name defaults to mesh.nc. + ./MpasMeshConverter.x [input_name] [output_name] + + input_name: + The input_name should be the name of a NetCDF file containing the following information. + Dimensions: + nCells - The number of cells described in the file. + nVertices - The number of vertices described in the file. + vertexDegree - The maximum number of cells that surround each vertex. + Variables: + xCell -- The x location of every cell center. + yCell -- The y location of every cell center. + zCell -- The z location of every cell center. + xVertex -- The x location of every vertex (cell corner). + yVertex -- The y location of every vertex (cell corner). + zVertex -- The z location of every vertex (cell corner). + cellsOnVertex -- A list of cell indices that border each vertex (Dimensioned vertexDegree * nVertices). + This list can contain -1 values if the dual cell is not complete (e.g. a line rather than a triangle). + meshDensity -- (Optional) The value of the generating density function at each cell center. + If this variable is ommitted, it is filled with 1.0 on output. + Global Attributes: + on_a_sphere -- Should be "YES" if the cells/vertices are defined on a sphere and "NO" if they are defined in a plane. + history -- (Optional) Should be defined to a string describing how the input file was generated. + mesh_id -- (Optional) Should be a 40 character string (Alpha-Numeric) that can be used to identify the mesh. + If ommitted, a random string will be placed in the output file. + output_name: + The output_name should be the name of the NetCDF file that will be generated. It will be a valid MPAS mesh. + No initial conditions are placed in this file. If input_name is specified, output_name defaults to mesh.nc. Usage of mpas_cell_culler.cpp: - ./MpasCellCuller.x [input_name] [output_name] [[-m/-i] mask_file] [-c] - - input_name: - The input name should be the name of a NetCDF file that is a fully valid MPAS file. - It is required to have all of the fields an output file from MpasMeshConverter.x contains. - There is one additional field: - cullCell -- (Optional) Should be dimensioned nCells. Should contain a 1 for all cells that should be removed - and a 0 for all cells that should be kept. - - output_name: - The output_name should be the name of the NetCDF file that will be generated. It will be the same as the input file, - with cells, edges, and vertices removed where appropriate. Also, the cullCell field will be removed. - If input_name is specified, outputname defaults to culled_mesh.nc. - - -m/-i: - These flags allow the addition of a masks file to be used to define - cells that should be culled. - If the -m flag is used, the masks file is used to cull the mesh. - Meaning, where any mask in the masks file is 1, the mesh will be - culled. - If the -i flag is used, the inverse of the masks file is used to cull - the mesh. Meaning where all masks in the masks file are 0, the mesh - will be culled. - - mask_file: - The mask_file argument should be the name of a NetCDF file containing - the regionCellMasks field, which defines masks for culling cells. - - -c: - Output the mapping from old to new mesh (cellMap) in cellMapForward.txt, - and output the reverse mapping from new to old mesh in cellMapBackward.txt. + ./MpasCellCuller.x [input_name] [output_name] [[-m/-i] mask_file] [-c] + + input_name: + The input name should be the name of a NetCDF file that is a fully valid MPAS file. + It is required to have all of the fields an output file from MpasMeshConverter.x contains. + There is one additional field: + cullCell -- (Optional) Should be dimensioned nCells. Should contain a 1 for all cells that should be removed + and a 0 for all cells that should be kept. + + output_name: + The output_name should be the name of the NetCDF file that will be generated. It will be the same as the input file, + with cells, edges, and vertices removed where appropriate. Also, the cullCell field will be removed. + If input_name is specified, outputname defaults to culled_mesh.nc. + + -m/-i: + These flags allow the addition of a masks file to be used to define + cells that should be culled. + If the -m flag is used, the masks file is used to cull the mesh. + Meaning, where any mask in the masks file is 1, the mesh will be + culled. + If the -i flag is used, the inverse of the masks file is used to cull + the mesh. Meaning where all masks in the masks file are 0, the mesh + will be culled. + + mask_file: + The mask_file argument should be the name of a NetCDF file containing + the regionCellMasks field, which defines masks for culling cells. + + -c: + Output the mapping from old to new mesh (cellMap) in cellMapForward.txt, + and output the reverse mapping from new to old mesh in cellMapBackward.txt. Usage of mpas_mask_creator.cpp: - ./MpasMaskCreator.x [input_mesh] [output_masks] [feature_group1] [feature_group2] ... [feature_groupN] - - input_name: - The input name should be the path to a NetCDF file that is a fully valid MPAS file. - - output_masks: - The output masks should be the path to a NetCDF file that will be - generated. It will contain all of the mask information after applying - the region specifications to the input name mesh file. - - feature_groupN: - After the output masks argument, the mask creator can take an arbitrary - number of feature files. The mask creator assumes each file is a group - of features (regions, transects, point data, etc.). Each file must be - formatted as geojson, and the name of each group should be contained - within the file. - - The mask creator assumes the latitude and longitude contained within - each geojson file are given in degrees, and that the latitude ranges - from -90 to 90, while the longitude ranges from -180 to 180. + ./MpasMaskCreator.x [input_mesh] [output_masks] [feature_group1] [feature_group2] ... [feature_groupN] + + input_name: + The input name should be the path to a NetCDF file that is a fully valid MPAS file. + + output_masks: + The output masks should be the path to a NetCDF file that will be + generated. It will contain all of the mask information after applying + the region specifications to the input name mesh file. + + feature_groupN: + After the output masks argument, the mask creator can take an arbitrary + number of feature files. The mask creator assumes each file is a group + of features (regions, transects, point data, etc.). Each file must be + formatted as geojson, and the name of each group should be contained + within the file. + + The mask creator assumes the latitude and longitude contained within + each geojson file are given in degrees, and that the latitude ranges + from -90 to 90, while the longitude ranges from -180 to 180. Notes for mpas_mesh_converter.cpp: - - The output mesh should have an attribute "mesh_spec" which defined which - version of the MPAS Mesh Specification this mesh conforms to. - - Cells that are not complete (e.g. are not surrounded by vertices) will be - given a negative area. - - Negative area cells can be removed at a later state to remove non-complete - cells. - + - The output mesh should have an attribute "mesh_spec" which defined which + version of the MPAS Mesh Specification this mesh conforms to. + - Cells that are not complete (e.g. are not surrounded by vertices) will be + given a negative area. + - Negative area cells can be removed at a later state to remove non-complete + cells. + Notes for mpas_cell_culler.cpp: - - Cells that contain negative area will be removed in addition to all cells that have - a cullCell value of 1. + - Cells that contain negative area will be removed in addition to all cells that have + a cullCell value of 1. diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h b/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h index dbf2d8789..d5ee339ef 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/edge.h @@ -1,32 +1,32 @@ class edge {/*{{{*/ - public: - int cell1, cell2; - int vertex1, vertex2; - int idx; + public: + int cell1, cell2; + int vertex1, vertex2; + int idx; - edge() - : cell1(-1), cell2(-1), vertex1(-1), vertex2(-1), idx(-1) { } + edge() + : cell1(-1), cell2(-1), vertex1(-1), vertex2(-1), idx(-1) { } - struct edge_hasher {/*{{{*/ - size_t operator()(const edge &edge_) const { - uint32_t hash; - size_t i, key[2] = { (size_t)edge_.vertex1, (size_t)edge_.vertex2 }; - for(hash = i = 0; i < sizeof(key); ++i) { - hash += ((uint8_t *)key)[i]; - hash += (hash << 10); - hash ^= (hash >> 6); - } - hash += (hash << 3); - hash ^= (hash >> 11); - hash += (hash << 15); - return hash; - } - };/*}}}*/ + struct edge_hasher {/*{{{*/ + size_t operator()(const edge &edge_) const { + uint32_t hash; + size_t i, key[2] = { (size_t)edge_.vertex1, (size_t)edge_.vertex2 }; + for(hash = i = 0; i < sizeof(key); ++i) { + hash += ((uint8_t *)key)[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + };/*}}}*/ - bool operator==(const edge &e) const {/*{{{*/ - return (vertex1 == e.vertex1) && (vertex2 == e.vertex2); - }/*}}}*/ + bool operator==(const edge &e) const {/*{{{*/ + return (vertex1 == e.vertex1) && (vertex2 == e.vertex2); + }/*}}}*/ };/*}}}*/ diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h index 910c7de97..4d97aed29 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json-forwards.h @@ -7,28 +7,28 @@ // ////////////////////////////////////////////////////////////////////// /* -The JsonCpp library's source code, including accompanying documentation, +The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... -The author (Baptiste Lepilleur) explicitly disclaims copyright in all -jurisdictions which recognize such a disclaimer. In such jurisdictions, +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is released under the terms of the MIT License (see below). -In jurisdictions which recognize Public Domain property, the user of this -software may choose to accept it either as 1) Public Domain, 2) under the -conditions of the MIT License (see below), or 3) under the terms of dual +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License - + The full text of the MIT License follows: ======================================================================== diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h index 01225cac6..73e29ec87 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/json/json.h @@ -6,28 +6,28 @@ // ////////////////////////////////////////////////////////////////////// /* -The JsonCpp library's source code, including accompanying documentation, +The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... -The author (Baptiste Lepilleur) explicitly disclaims copyright in all -jurisdictions which recognize such a disclaimer. In such jurisdictions, +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is released under the terms of the MIT License (see below). -In jurisdictions which recognize Public Domain property, the user of this -software may choose to accept it either as 1) Public Domain, 2) under the -conditions of the MIT License (see below), or 3) under the terms of dual +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License - + The full text of the MIT License follows: ======================================================================== @@ -434,7 +434,7 @@ class JSON_API Exception : public std::exception { /** Exceptions which the user cannot easily avoid. * * E.g. out-of-memory (when we use malloc), stack-overflow, malicious input - * + * * \remark derived from Json::Exception */ class JSON_API RuntimeError : public Exception { @@ -445,7 +445,7 @@ class JSON_API RuntimeError : public Exception { /** Exceptions thrown by JSON_ASSERT/JSON_FAIL macros. * * These are precondition-violations (user bugs) and internal errors (our bugs). - * + * * \remark derived from Json::Exception */ class JSON_API LogicError : public Exception { @@ -1570,7 +1570,7 @@ class JSON_API CharReaderBuilder : public CharReader::Factory { - `"rejectDupKeys": false or true` - If true, `parse()` returns false when a key is duplicated within an object. - `"allowSpecialFloats": false or true` - - If true, special float values (NaNs and infinities) are allowed + - If true, special float values (NaNs and infinities) are allowed and their values are lossfree restorable. You can examine 'settings_` yourself diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp index 985589c82..847f66f9c 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/jsoncpp.cpp @@ -6,28 +6,28 @@ // ////////////////////////////////////////////////////////////////////// /* -The JsonCpp library's source code, including accompanying documentation, +The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... -The author (Baptiste Lepilleur) explicitly disclaims copyright in all -jurisdictions which recognize such a disclaimer. In such jurisdictions, +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is released under the terms of the MIT License (see below). -In jurisdictions which recognize Public Domain property, the user of this -software may choose to accept it either as 1) Public Domain, 2) under the -conditions of the MIT License (see below), or 3) under the terms of dual +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License - + The full text of the MIT License follows: ======================================================================== @@ -207,7 +207,7 @@ static inline void fixNumericLocale(char* begin, char* end) { #include #if defined(_MSC_VER) -#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above +#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above #define snprintf sprintf_s #elif _MSC_VER >= 1900 // VC++ 14.0 and above #define snprintf std::snprintf @@ -4029,7 +4029,7 @@ Value& Path::make(Value& root) const { #define snprintf std::snprintf #endif -#if defined(__BORLANDC__) +#if defined(__BORLANDC__) #include #define isfinite _finite #define snprintf _snprintf @@ -5096,7 +5096,7 @@ StreamWriter* StreamWriterBuilder::newStreamWriter() const std::string cs_str = settings_["commentStyle"].asString(); bool eyc = settings_["enableYAMLCompatibility"].asBool(); bool dnp = settings_["dropNullPlaceholders"].asBool(); - bool usf = settings_["useSpecialFloats"].asBool(); + bool usf = settings_["useSpecialFloats"].asBool(); unsigned int pre = settings_["precision"].asUInt(); CommentStyle::Enum cs = CommentStyle::All; if (cs_str == "All") { diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index b28be6675..8185745c6 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -54,7 +54,7 @@ int markEdges(); int outputGridDimensions(const string outputFilename); int outputGridAttributes(const string inputFilename, const string outputFilename); int mapAndOutputGridCoordinates(const string inputFilename, const string outputFilename); -int mapAndOutputCellFields(const string inputFilename, const string outputPath, +int mapAndOutputCellFields(const string inputFilename, const string outputPath, const string outputFilename); int mapAndOutputEdgeFields(const string inputFilename, const string outputFilename); int mapAndOutputVertexFields(const string inputFilename, const string outputFilename); @@ -62,263 +62,263 @@ int outputCellMap(const string outputPath); /*}}}*/ void print_usage(){/*{{{*/ - cout << endl << endl; - cout << "Usage:" << endl; - cout << "\tMpasCellCuller.x [input_name] [output_name] [[-m/-i/-p] masks_name] [-c]" << endl; - cout << endl; - cout << "\t\tinput_name:" << endl; - cout << "\t\t\tThis argument specifies the input MPAS mesh." << endl; - cout << "\t\toutput_name:" << endl; - cout << "\t\t\tThis argument specifies the output culled MPAS mesh." << endl; - cout << "\t\t\tIf not specified, it defaults to culled_mesh.nc, but" << endl; - cout << "\t\t\tit is required if additional arguments are specified." << endl; - cout << "\t\t-m/-i/-p:" << endl; - cout << "\t\t\tThese arguments control how a set of masks is used when" << endl; + cout << endl << endl; + cout << "Usage:" << endl; + cout << "\tMpasCellCuller.x [input_name] [output_name] [[-m/-i/-p] masks_name] [-c]" << endl; + cout << endl; + cout << "\t\tinput_name:" << endl; + cout << "\t\t\tThis argument specifies the input MPAS mesh." << endl; + cout << "\t\toutput_name:" << endl; + cout << "\t\t\tThis argument specifies the output culled MPAS mesh." << endl; + cout << "\t\t\tIf not specified, it defaults to culled_mesh.nc, but" << endl; + cout << "\t\t\tit is required if additional arguments are specified." << endl; + cout << "\t\t-m/-i/-p:" << endl; + cout << "\t\t\tThese arguments control how a set of masks is used when" << endl; cout << "\t\t\tculling a mesh." << endl; - cout << "\t\t\tThe -m argument applies a mask to cull based on (i.e." << endl; + cout << "\t\t\tThe -m argument applies a mask to cull based on (i.e." << endl; cout << "\t\t\twhere the mask is 1, the mesh will be culled)." << endl; - cout << "\t\t\tThe -i argument applies the inverse mask to cull based" << endl; + cout << "\t\t\tThe -i argument applies the inverse mask to cull based" << endl; cout << "\t\t\ton (i.e. where the mask is 0, the mesh will be" << endl; cout << "\t\t\tculled)." << endl; - cout << "\t\t\tThe -p argument forces any marked cells to not be" << endl; + cout << "\t\t\tThe -p argument forces any marked cells to not be" << endl; cout << "\t\t\tculled." << endl; - cout << "\t\t\tIf this argument is specified, the masks_name argument" << endl; + cout << "\t\t\tIf this argument is specified, the masks_name argument" << endl; cout << "\t\t\tis required" << endl; - cout << "\t\t-c:" << endl; - cout << "\t\t\tOutput the mapping from old to new mesh (cellMap) in" << endl; + cout << "\t\t-c:" << endl; + cout << "\t\t\tOutput the mapping from old to new mesh (cellMap) in" << endl; cout << "\t\t\t\tcellMapForward.txt, " << endl; - cout << "\t\t\tand output the reverse mapping from new to old mesh in" << endl; + cout << "\t\t\tand output the reverse mapping from new to old mesh in" << endl; cout << "\t\t\t\tcellMapBackward.txt." << endl; }/*}}}*/ string gen_random(const int len); int main ( int argc, char *argv[] ) { - int error; - string out_name = "culled_mesh.nc"; - string in_name = "mesh.nc"; + int error; + string out_name = "culled_mesh.nc"; + string in_name = "mesh.nc"; string out_path = ""; string out_file = ""; string out_fext = ""; - vector mask_names; - vector mask_ops; - - cout << endl << endl; - cout << "************************************************************" << endl; - cout << "MPAS_CELL_CULLER:\n"; - cout << " C++ version\n"; - cout << " Remove cells/edges/vertices from a NetCDF MPAS Mesh file. \n"; - cout << endl << endl; - cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; - cout << "************************************************************" << endl; - cout << endl << endl; - // - // If the input file was not specified, get it now. - // - if ( argc <= 1 ) - { - cout << "\n"; - cout << "MPAS_CELL_CULLER:\n"; - cout << " Please enter the NetCDF input filename.\n"; - - cin >> in_name; - - cout << "\n"; - cout << "MPAS_CELL_CULLER:\n"; - cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; - - cin >> out_name; - } - else if (argc == 2) - { - in_name = argv[1]; - - cout << "\n"; - cout << "MPAS_CELL_CULLER:\n"; - cout << " Output name not specified. Using default of culled_mesh.nc\n"; - } - else if (argc == 3) - { - in_name = argv[1]; - out_name = argv[2]; - } - else if (argc >= 10) - { - cout << "\n"; - cout << " ERROR: Incorrect number of arguments specified. See usage statement" << endl; - print_usage(); - exit(1); - } - else - { - cullMasks = true; - in_name = argv[1]; - out_name = argv[2]; - bool foundOperation; - - for ( int i = 3; i < argc; i+=2 ) { - foundOperation = false; - if (strcmp(argv[i], "-m") == 0 ) { - mask_ops.push_back(static_cast(mergeOp)); - foundOperation = true; - } else if ( strcmp(argv[i], "-i") == 0 ){ - mask_ops.push_back(static_cast(invertOp)); - foundOperation = true; - } else if ( strcmp(argv[i], "-p") == 0 ){ - mask_ops.push_back(static_cast(preserveOp)); - foundOperation = true; - } else if ( strcmp(argv[i], "-c") == 0 ){ - outputMap = true; - } else { - cout << " ERROR: Invalid option passed on the command line " << argv[i] << ". Exiting..." << endl; - print_usage(); - exit(1); - } - - if (foundOperation) { - mask_names.push_back( argv[i+1] ); - } - } - } - - if(out_name == in_name){ - cout << " ERROR: Input and Output names are the same." << endl; - return 1; - } + vector mask_names; + vector mask_ops; + + cout << endl << endl; + cout << "************************************************************" << endl; + cout << "MPAS_CELL_CULLER:\n"; + cout << " C++ version\n"; + cout << " Remove cells/edges/vertices from a NetCDF MPAS Mesh file. \n"; + cout << endl << endl; + cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; + cout << "************************************************************" << endl; + cout << endl << endl; + // + // If the input file was not specified, get it now. + // + if ( argc <= 1 ) + { + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Please enter the NetCDF input filename.\n"; + + cin >> in_name; + + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; + + cin >> out_name; + } + else if (argc == 2) + { + in_name = argv[1]; + + cout << "\n"; + cout << "MPAS_CELL_CULLER:\n"; + cout << " Output name not specified. Using default of culled_mesh.nc\n"; + } + else if (argc == 3) + { + in_name = argv[1]; + out_name = argv[2]; + } + else if (argc >= 10) + { + cout << "\n"; + cout << " ERROR: Incorrect number of arguments specified. See usage statement" << endl; + print_usage(); + exit(1); + } + else + { + cullMasks = true; + in_name = argv[1]; + out_name = argv[2]; + bool foundOperation; + + for ( int i = 3; i < argc; i+=2 ) { + foundOperation = false; + if (strcmp(argv[i], "-m") == 0 ) { + mask_ops.push_back(static_cast(mergeOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-i") == 0 ){ + mask_ops.push_back(static_cast(invertOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-p") == 0 ){ + mask_ops.push_back(static_cast(preserveOp)); + foundOperation = true; + } else if ( strcmp(argv[i], "-c") == 0 ){ + outputMap = true; + } else { + cout << " ERROR: Invalid option passed on the command line " << argv[i] << ". Exiting..." << endl; + print_usage(); + exit(1); + } + + if (foundOperation) { + mask_names.push_back( argv[i+1] ); + } + } + } + + if(out_name == in_name){ + cout << " ERROR: Input and Output names are the same." << endl; + return 1; + } file_part(out_name, out_path, out_file, out_fext); - srand(time(NULL)); - - cout << "Reading input grid." << endl; - error = readGridInput(in_name); - if(error) return 1; - - if ( cullMasks ) { - cout << "Reading in mask information." << endl; - for ( int i = 0; i < mask_names.size(); i++ ) { - error = mergeCellMasks(mask_names[i], mask_ops[i]); - if(error) return 1; - } - } - - cout << "Marking cells for removal." << endl; - error = markCells(); - if(error) return 1; - - cout << "Marking vertices for removal." << endl; - error = markVertices(); - if(error) return 1; - - cout << "Marking edges for removal." << endl; - error = markEdges(); - if(error) return 1; - - cout << "Writing grid dimensions" << endl; - if(error = outputGridDimensions(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Writing grid attributes" << endl; - if(error = outputGridAttributes(in_name, out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Writing grid coordinates" << endl; - if(error = mapAndOutputGridCoordinates(in_name, out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Mapping and writing cell fields and culled_graph.info" << endl; - if(error = mapAndOutputCellFields(in_name, out_path, out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Mapping and writing edge fields" << endl; - if(error = mapAndOutputEdgeFields(in_name, out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Mapping and writing vertex fields" << endl; - if(error = mapAndOutputVertexFields(in_name, out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Outputting cell map" << endl; - if (outputMap) { - if(error = outputCellMap(out_path)){ - cout << "Error - " << error << endl; - exit(error); - } - } - - return 0; + srand(time(NULL)); + + cout << "Reading input grid." << endl; + error = readGridInput(in_name); + if(error) return 1; + + if ( cullMasks ) { + cout << "Reading in mask information." << endl; + for ( int i = 0; i < mask_names.size(); i++ ) { + error = mergeCellMasks(mask_names[i], mask_ops[i]); + if(error) return 1; + } + } + + cout << "Marking cells for removal." << endl; + error = markCells(); + if(error) return 1; + + cout << "Marking vertices for removal." << endl; + error = markVertices(); + if(error) return 1; + + cout << "Marking edges for removal." << endl; + error = markEdges(); + if(error) return 1; + + cout << "Writing grid dimensions" << endl; + if(error = outputGridDimensions(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing grid attributes" << endl; + if(error = outputGridAttributes(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing grid coordinates" << endl; + if(error = mapAndOutputGridCoordinates(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing cell fields and culled_graph.info" << endl; + if(error = mapAndOutputCellFields(in_name, out_path, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing edge fields" << endl; + if(error = mapAndOutputEdgeFields(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Mapping and writing vertex fields" << endl; + if(error = mapAndOutputVertexFields(in_name, out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Outputting cell map" << endl; + if (outputMap) { + if(error = outputCellMap(out_path)){ + cout << "Error - " << error << endl; + exit(error); + } + } + + return 0; } int outputCellMap(const string outputPath){/*{{{*/ - int iCell; - ofstream outputfileForward, outputfileBackward; + int iCell; + ofstream outputfileForward, outputfileBackward; - // forwards mapping - outputfileForward.open(path_join(outputPath, "cellMapForward.txt")); + // forwards mapping + outputfileForward.open(path_join(outputPath, "cellMapForward.txt")); - for (iCell=0 ; iCell < nCells ; iCell++) { + for (iCell=0 ; iCell < nCells ; iCell++) { - outputfileForward << cellMap.at(iCell) << endl; + outputfileForward << cellMap.at(iCell) << endl; - } + } - outputfileForward.close(); + outputfileForward.close(); - // backwards mapping - int nCellsNew = 0; - vector cellMapBackward; + // backwards mapping + int nCellsNew = 0; + vector cellMapBackward; - cellMapBackward.clear(); - cellMapBackward.resize(nCells); + cellMapBackward.clear(); + cellMapBackward.resize(nCells); - for (iCell=0 ; iCell < nCells ; iCell++) { + for (iCell=0 ; iCell < nCells ; iCell++) { - if (cellMap.at(iCell) >= 0) { + if (cellMap.at(iCell) >= 0) { - cellMapBackward.at(cellMap.at(iCell)) = iCell; - nCellsNew++; + cellMapBackward.at(cellMap.at(iCell)) = iCell; + nCellsNew++; - } + } - } + } - outputfileBackward.open(path_join(outputPath, "cellMapBackward.txt")); + outputfileBackward.open(path_join(outputPath, "cellMapBackward.txt")); - for (iCell=0 ; iCell < nCellsNew ; iCell++) { + for (iCell=0 ; iCell < nCellsNew ; iCell++) { - outputfileBackward << cellMapBackward.at(iCell) << endl; + outputfileBackward << cellMapBackward.at(iCell) << endl; - } + } - outputfileBackward.close(); + outputfileBackward.close(); - cellMapBackward.clear(); + cellMapBackward.clear(); - return 0; + return 0; }/*}}}*/ /* Input/Marking Functions {{{ */ int readGridInput(const string inputFilename){/*{{{*/ - double *xcell, *ycell,*zcell; - double *xvertex, *yvertex,*zvertex; - int *cellsonvertex_list, *verticesonedge_list, *cellsonedge_list; + double *xcell, *ycell,*zcell; + double *xvertex, *yvertex,*zvertex; + int *cellsonvertex_list, *verticesonedge_list, *cellsonedge_list; string on_a_sphere, is_periodic; #ifdef _DEBUG - cout << endl << endl << "Begin function: readGridInput" << endl << endl; + cout << endl << endl << "Begin function: readGridInput" << endl << endl; #endif ncutil::get_dim(inputFilename, "nCells", nCells); @@ -329,133 +329,133 @@ int readGridInput(const string inputFilename){/*{{{*/ try { #ifdef _DEBUG - cout << " Reading on_a_sphere" << endl; + cout << " Reading on_a_sphere" << endl; #endif ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); - spherical = (on_a_sphere.find("YES") != string::npos); + spherical = (on_a_sphere.find("YES") != string::npos); #ifdef _DEBUG - cout << " Reading sphere_radius" << endl; + cout << " Reading sphere_radius" << endl; #endif ncutil::get_att(inputFilename, "sphere_radius", &sphere_radius); #ifdef _DEBUG - cout << " Reading history" << endl; + cout << " Reading history" << endl; #endif ncutil::get_str(inputFilename, "history", in_history); #ifdef _DEBUG - cout << " Reading file_id" << endl; + cout << " Reading file_id" << endl; #endif ncutil::get_str(inputFilename, "file_id", in_file_id); #ifdef _DEBUG - cout << " Reading parent_id" << endl; + cout << " Reading parent_id" << endl; #endif - ncutil::get_str(inputFilename, "parent_id", in_parent_id); + ncutil::get_str(inputFilename, "parent_id", in_parent_id); #ifdef _DEBUG - cout << " Reading parent_id" << endl; + cout << " Reading parent_id" << endl; #endif - ncutil::get_str(inputFilename, "mesh_spec", in_mesh_spec); + ncutil::get_str(inputFilename, "mesh_spec", in_mesh_spec); #ifdef _DEBUG - cout << " Reading is_periodic" << endl; + cout << " Reading is_periodic" << endl; #endif ncutil::get_str(inputFilename, "is_periodic", is_periodic); periodic = (is_periodic.find("YES") != string::npos); #ifdef _DEBUG - cout << " Reading x_period" << endl; + cout << " Reading x_period" << endl; #endif - ncutil::get_att(inputFilename, "x_period", &xPeriod); + ncutil::get_att(inputFilename, "x_period", &xPeriod); #ifdef _DEBUG - cout << " Reading y_period" << endl; + cout << " Reading y_period" << endl; #endif - ncutil::get_att(inputFilename, "y_period", &yPeriod); + ncutil::get_att(inputFilename, "y_period", &yPeriod); } catch (...) { // allow errors for optional attr. not found } - cout << "Read dimensions:" << endl; - cout << " nCells = " << nCells << endl; - cout << " nVertices = " << nVertices << endl; - cout << " nEdges = " << nEdges << endl; - cout << " vertexDegree = " << vertexDegree << endl; - cout << " maxEdges = " << maxEdges << endl; - cout << " Spherical? = " << spherical << endl; - cout << " Periodic? = " << periodic << endl; - if ( periodic ) { - cout << " x_period = " << xPeriod << endl; - cout << " y_period = " << yPeriod << endl; - } + cout << "Read dimensions:" << endl; + cout << " nCells = " << nCells << endl; + cout << " nVertices = " << nVertices << endl; + cout << " nEdges = " << nEdges << endl; + cout << " vertexDegree = " << vertexDegree << endl; + cout << " maxEdges = " << maxEdges << endl; + cout << " Spherical? = " << spherical << endl; + cout << " Periodic? = " << periodic << endl; + if ( periodic ) { + cout << " x_period = " << xPeriod << endl; + cout << " y_period = " << yPeriod << endl; + } #ifdef _DEBUG - cout << " Read areaCell" << endl; + cout << " Read areaCell" << endl; #endif - areaCell.clear(); - areaCell.resize(nCells); + areaCell.clear(); + areaCell.resize(nCells); ncutil::get_var(inputFilename, "areaCell", &areaCell[0]); #ifdef _DEBUG - cout << " Read cullCell" << endl; + cout << " Read cullCell" << endl; #endif - cullCell.clear(); - cullCell.resize(nCells); + cullCell.clear(); + cullCell.resize(nCells); ncutil::get_var(inputFilename, "cullCell", &cullCell[0]); - // Build cellsOnVertex information - cellsonvertex_list = new int[nVertices * vertexDegree]; + // Build cellsOnVertex information + cellsonvertex_list = new int[nVertices * vertexDegree]; #ifdef _DEBUG - cout << " Read cellsOnVertex" << endl; + cout << " Read cellsOnVertex" << endl; #endif ncutil::get_var(inputFilename, "cellsOnVertex", cellsonvertex_list); - cellsOnVertex.resize(nVertices); + cellsOnVertex.resize(nVertices); - for(int i = 0; i < nVertices; i++){ - for(int j = 0; j < vertexDegree; j++){ - // Subtract 1 to convert into base 0 (c index space). - cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); - } - } + for(int i = 0; i < nVertices; i++){ + for(int j = 0; j < vertexDegree; j++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); + } + } - delete[] cellsonvertex_list; + delete[] cellsonvertex_list; - // Build verticesOnEdge information - verticesonedge_list = new int[nEdges * 2]; - verticesOnEdge.clear(); - verticesOnEdge.resize(nEdges); + // Build verticesOnEdge information + verticesonedge_list = new int[nEdges * 2]; + verticesOnEdge.clear(); + verticesOnEdge.resize(nEdges); #ifdef _DEBUG - cout << " Read verticesOnEdge" << endl; + cout << " Read verticesOnEdge" << endl; #endif ncutil::get_var(inputFilename, "verticesOnEdge", verticesonedge_list); - for(int i = 0; i < nEdges; i++){ - // Subtract 1 to convert into base 0 (c index space). - verticesOnEdge.at(i).push_back(verticesonedge_list[i*2] - 1); - verticesOnEdge.at(i).push_back(verticesonedge_list[i*2+1] - 1); - } + for(int i = 0; i < nEdges; i++){ + // Subtract 1 to convert into base 0 (c index space). + verticesOnEdge.at(i).push_back(verticesonedge_list[i*2] - 1); + verticesOnEdge.at(i).push_back(verticesonedge_list[i*2+1] - 1); + } - delete[] verticesonedge_list; + delete[] verticesonedge_list; - // Build cellsOnEdge information - cellsonedge_list = new int[nEdges * 2]; - cellsOnEdge.clear(); - cellsOnEdge.resize(nEdges); + // Build cellsOnEdge information + cellsonedge_list = new int[nEdges * 2]; + cellsOnEdge.clear(); + cellsOnEdge.resize(nEdges); #ifdef _DEBUG - cout << " Read cellsOnEdge" << endl; + cout << " Read cellsOnEdge" << endl; #endif ncutil::get_var(inputFilename, "cellsOnEdge", cellsonedge_list); - for(int i = 0; i < nEdges; i++){ - // Subtract 1 to convert into base 0 (c index space). - cellsOnEdge.at(i).push_back(cellsonedge_list[i*2] - 1); - cellsOnEdge.at(i).push_back(cellsonedge_list[i*2+1] - 1); - } + for(int i = 0; i < nEdges; i++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnEdge.at(i).push_back(cellsonedge_list[i*2] - 1); + cellsOnEdge.at(i).push_back(cellsonedge_list[i*2+1] - 1); + } - delete[] cellsonedge_list; + delete[] cellsonedge_list; - return 0; + return 0; }/*}}}*/ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ - size_t nRegions = 0, nTransects = 0; - int *regionCellMasks, *transectCellMasks, *cellSeedMask, *flattenedMask; - int i, j; + size_t nRegions = 0, nTransects = 0; + int *regionCellMasks, *transectCellMasks, *cellSeedMask, *flattenedMask; + int i, j; try { ncutil::get_dim(masksFilename, "nRegions", nRegions); @@ -464,10 +464,10 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ // allow errors for optional attr. not found } - regionCellMasks = new int[nCells*nRegions]; - transectCellMasks = new int[nCells*nTransects]; - cellSeedMask = new int[nCells]; - flattenedMask = new int[nCells]; + regionCellMasks = new int[nCells*nRegions]; + transectCellMasks = new int[nCells*nTransects]; + cellSeedMask = new int[nCells]; + flattenedMask = new int[nCells]; try { ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); @@ -477,156 +477,156 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ // allow errors for optional vars. not found } - for ( i = 0; i < nCells; i++){ - flattenedMask[i] = cellSeedMask[i]; - for ( j = 0; j < nRegions; j++){ - flattenedMask[i] = max(flattenedMask[i], regionCellMasks[i * nRegions + j]); - } - - for ( j = 0; j < nTransects; j++ ) { - flattenedMask[i] = max(flattenedMask[i], transectCellMasks[i * nTransects + j]); - } - } - - if ( maskOp == invertOp || maskOp == mergeOp ) { - if ( maskOp == invertOp ) { - for (i = 0; i < nCells; i++){ - flattenedMask[i] = (flattenedMask[i] + 1) % 2; - } - } - - for ( i = 0; i < nCells; i++ ){ - cullCell[i] = max(cullCell[i], flattenedMask[i]); - } - } else if ( maskOp == preserveOp ) { - for ( i = 0; i < nCells; i++ ) { - if ( flattenedMask[i] && cullCell[i] ) { - cullCell[i] = 0; - } - } - } - - delete[] cellSeedMask; - delete[] transectCellMasks; - delete[] regionCellMasks; - delete[] flattenedMask; - - return 0; + for ( i = 0; i < nCells; i++){ + flattenedMask[i] = cellSeedMask[i]; + for ( j = 0; j < nRegions; j++){ + flattenedMask[i] = max(flattenedMask[i], regionCellMasks[i * nRegions + j]); + } + + for ( j = 0; j < nTransects; j++ ) { + flattenedMask[i] = max(flattenedMask[i], transectCellMasks[i * nTransects + j]); + } + } + + if ( maskOp == invertOp || maskOp == mergeOp ) { + if ( maskOp == invertOp ) { + for (i = 0; i < nCells; i++){ + flattenedMask[i] = (flattenedMask[i] + 1) % 2; + } + } + + for ( i = 0; i < nCells; i++ ){ + cullCell[i] = max(cullCell[i], flattenedMask[i]); + } + } else if ( maskOp == preserveOp ) { + for ( i = 0; i < nCells; i++ ) { + if ( flattenedMask[i] && cullCell[i] ) { + cullCell[i] = 0; + } + } + } + + delete[] cellSeedMask; + delete[] transectCellMasks; + delete[] regionCellMasks; + delete[] flattenedMask; + + return 0; }/*}}}*/ int markCells(){/*{{{*/ - int new_idx; - int cells_removed; - cellMap.clear(); - cellMap.resize(nCells); - - new_idx = 0; - cells_removed = 0; - for(int iCell = 0; iCell < nCells; iCell++){ - // Remove all cells with negative area, and cells that shouldn't be in the grid. - if(areaCell.at(iCell) < 0 || cullCell.at(iCell) == 1){ - cellMap.at(iCell) = -1; - cells_removed++; - } else { - cellMap.at(iCell) = new_idx; - new_idx++; - } - } - - cout << "Removing " << cells_removed << " cells." << endl; - - return 0; + int new_idx; + int cells_removed; + cellMap.clear(); + cellMap.resize(nCells); + + new_idx = 0; + cells_removed = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + // Remove all cells with negative area, and cells that shouldn't be in the grid. + if(areaCell.at(iCell) < 0 || cullCell.at(iCell) == 1){ + cellMap.at(iCell) = -1; + cells_removed++; + } else { + cellMap.at(iCell) = new_idx; + new_idx++; + } + } + + cout << "Removing " << cells_removed << " cells." << endl; + + return 0; }/*}}}*/ int markVertices(){/*{{{*/ - bool keep_vertex; - int iCell, new_idx; - int vertices_removed; - - vertexMap.clear(); - vertexMap.resize(nVertices); - - new_idx = 0; - vertices_removed = 0; - for(int iVertex = 0; iVertex < nVertices; iVertex++){ - - // Only keep vertices that have at least one cell connected to them - // after cell removal. - keep_vertex = false; - for(int j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ - iCell = cellsOnVertex.at(iVertex).at(j); - if(iCell != -1) { - keep_vertex = keep_vertex || (cellMap.at(iCell) != -1); - } - } - - if(keep_vertex){ - vertexMap.at(iVertex) = new_idx; - new_idx++; - } else { - vertexMap.at(iVertex) = -1; - vertices_removed++; - } - } - - cout << "Removing " << vertices_removed << " vertices." << endl; - - return 0; + bool keep_vertex; + int iCell, new_idx; + int vertices_removed; + + vertexMap.clear(); + vertexMap.resize(nVertices); + + new_idx = 0; + vertices_removed = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + + // Only keep vertices that have at least one cell connected to them + // after cell removal. + keep_vertex = false; + for(int j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + iCell = cellsOnVertex.at(iVertex).at(j); + if(iCell != -1) { + keep_vertex = keep_vertex || (cellMap.at(iCell) != -1); + } + } + + if(keep_vertex){ + vertexMap.at(iVertex) = new_idx; + new_idx++; + } else { + vertexMap.at(iVertex) = -1; + vertices_removed++; + } + } + + cout << "Removing " << vertices_removed << " vertices." << endl; + + return 0; }/*}}}*/ int markEdges(){/*{{{*/ - bool keep_edge; - int vertex1, vertex2; - int cell1, cell2; - int new_idx; - int edges_removed; - - edgeMap.clear(); - edgeMap.resize(nEdges); - - new_idx = 0; - edges_removed = 0; - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - vertex1 = verticesOnEdge.at(iEdge).at(0); - vertex2 = verticesOnEdge.at(iEdge).at(1); - cell1 = cellsOnEdge.at(iEdge).at(0); - cell2 = cellsOnEdge.at(iEdge).at(1); - - // Only keep an edge if it has two vertices - // after vertex removal and at least one cell - // after cell removal. - if(vertex2 != -1){ - keep_edge = (vertexMap.at(vertex1) != -1 && vertexMap.at(vertex2) != -1); - } else { - keep_edge = false; - } - - if(cell2 != -1){ - keep_edge = keep_edge && (cellMap.at(cell1) != -1 || cellMap.at(cell2) != -1); - } else { - keep_edge = keep_edge && (cellMap.at(cell1) != -1); - } - - if(keep_edge){ - edgeMap.at(iEdge) = new_idx; - new_idx++; - } else { - edgeMap.at(iEdge) = -1; - edges_removed++; - } - } - - cout << "Removing " << edges_removed << " edges." << endl; - - return 0; + bool keep_edge; + int vertex1, vertex2; + int cell1, cell2; + int new_idx; + int edges_removed; + + edgeMap.clear(); + edgeMap.resize(nEdges); + + new_idx = 0; + edges_removed = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + + // Only keep an edge if it has two vertices + // after vertex removal and at least one cell + // after cell removal. + if(vertex2 != -1){ + keep_edge = (vertexMap.at(vertex1) != -1 && vertexMap.at(vertex2) != -1); + } else { + keep_edge = false; + } + + if(cell2 != -1){ + keep_edge = keep_edge && (cellMap.at(cell1) != -1 || cellMap.at(cell2) != -1); + } else { + keep_edge = keep_edge && (cellMap.at(cell1) != -1); + } + + if(keep_edge){ + edgeMap.at(iEdge) = new_idx; + new_idx++; + } else { + edgeMap.at(iEdge) = -1; + edges_removed++; + } + } + + cout << "Removing " << edges_removed << " edges." << endl; + + return 0; }/*}}}*/ /*}}}*/ /* Mapping/Output functions {{{*/ int outputGridDimensions( const string outputFilename ){/*{{{*/ - /************************************************************************ - * - * This function writes the grid dimensions to the netcdf file named - * outputFilename - * - * **********************************************************************/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ int grid, retv; if ((retv = nc_create(outputFilename.c_str(), NC_CLOBBER|NC_NETCDF4, &grid))) @@ -636,21 +636,21 @@ int outputGridDimensions( const string outputFilename ){/*{{{*/ } size_t nCellsNew, nEdgesNew, nVerticesNew; - + nCellsNew = 0; - for(int iCell = 0; iCell < nCells; iCell++){ - nCellsNew += (cellMap.at(iCell) != -1); - } + for(int iCell = 0; iCell < nCells; iCell++){ + nCellsNew += (cellMap.at(iCell) != -1); + } - nVerticesNew = 0; - for(int iVertex = 0; iVertex < nVertices; iVertex++){ - nVerticesNew += (vertexMap.at(iVertex) != -1); - } + nVerticesNew = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + nVerticesNew += (vertexMap.at(iVertex) != -1); + } - nEdgesNew = 0; - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - nEdgesNew += (edgeMap.at(iEdge) != -1); - } + nEdgesNew = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + nEdgesNew += (edgeMap.at(iEdge) != -1); + } ncutil::def_dim(outputFilename, "nCells", nCellsNew); ncutil::def_dim(outputFilename, "nEdges", nEdgesNew); @@ -659,57 +659,57 @@ int outputGridDimensions( const string outputFilename ){/*{{{*/ ncutil::def_dim(outputFilename, "vertexDegree", vertexDegree); ncutil::def_dim(outputFilename, "Time", NC_UNLIMITED); - return 0; + return 0; }/*}}}*/ int outputGridAttributes( const string inputFilename, const string outputFilename ){/*{{{*/ - /************************************************************************ - * - * This function writes the grid dimensions to the netcdf file named - * outputFilename - * - * **********************************************************************/ - - string history_str = ""; - string id_str = ""; - string parent_str = ""; - - // write attributes - if(!spherical){ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + string history_str = ""; + string id_str = ""; + string parent_str = ""; + + // write attributes + if(!spherical){ ncutil::put_str(outputFilename, "on_a_sphere", "NO"); ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, 0.); - } else { + } else { ncutil::put_str(outputFilename, "on_a_sphere", "YES"); - ncutil::put_att(outputFilename, + ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, sphere_radius); - } + } - if(!periodic){ + if(!periodic){ ncutil::put_str(outputFilename, "is_periodic", "NO"); - } else { + } else { ncutil::put_str(outputFilename, "is_periodic", "YES"); ncutil::put_att(outputFilename, "x_period", NC_DOUBLE, xPeriod); ncutil::put_att(outputFilename, "y_period", NC_DOUBLE, yPeriod); - } - - history_str += "MpasCellCuller.x "; - history_str += inputFilename; - history_str += " "; - history_str += outputFilename; - if(in_history != ""){ - history_str += "\n"; - history_str += in_history; - } - - if(in_file_id != "" ){ - parent_str = in_file_id; - if(in_parent_id != ""){ - parent_str += "\n"; - parent_str += in_parent_id; - } + } + + history_str += "MpasCellCuller.x "; + history_str += inputFilename; + history_str += " "; + history_str += outputFilename; + if(in_history != ""){ + history_str += "\n"; + history_str += in_history; + } + + if(in_file_id != "" ){ + parent_str = in_file_id; + if(in_parent_id != ""){ + parent_str += "\n"; + parent_str += in_parent_id; + } ncutil::put_str(outputFilename, "parent_id", parent_str); - } + } - id_str = gen_random(ID_LEN); + id_str = gen_random(ID_LEN); ncutil::put_str(outputFilename, "history", history_str); ncutil::put_str(outputFilename, "mesh_spec", in_mesh_spec); @@ -717,767 +717,767 @@ int outputGridAttributes( const string inputFilename, const string outputFilenam ncutil::put_str(outputFilename, "source", "MpasCellCuller.x"); ncutil::put_str(outputFilename, "file_id", id_str); - return 0; + return 0; }/*}}}*/ int mapAndOutputGridCoordinates( const string inputFilename, const string outputFilename) {/*{{{*/ - /************************************************************************ - * - * This function writes the grid coordinates to the netcdf file named - * outputFilename - * This includes all cell centers, vertices, and edges. - * Both cartesian and lat,lon, as well as all of their indices - * - * **********************************************************************/ - - size_t nCellsNew, nEdgesNew, nVerticesNew; + /************************************************************************ + * + * This function writes the grid coordinates to the netcdf file named + * outputFilename + * This includes all cell centers, vertices, and edges. + * Both cartesian and lat,lon, as well as all of their indices + * + * **********************************************************************/ + + size_t nCellsNew, nEdgesNew, nVerticesNew; ncutil::get_dim(outputFilename, "nCells", nCellsNew); ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); ncutil::get_dim(outputFilename, "nVertices", nVerticesNew); - int i, idx_map; + int i, idx_map; - double *xOld, *yOld, *zOld, *latOld, *lonOld; - double *xNew, *yNew, *zNew, *latNew, *lonNew; - int *idxToNew; + double *xOld, *yOld, *zOld, *latOld, *lonOld; + double *xNew, *yNew, *zNew, *latNew, *lonNew; + int *idxToNew; - // Build and write cell coordinate arrays - xOld = new double[nCells]; - yOld = new double[nCells]; - zOld = new double[nCells]; - latOld = new double[nCells]; - lonOld = new double[nCells]; + // Build and write cell coordinate arrays + xOld = new double[nCells]; + yOld = new double[nCells]; + zOld = new double[nCells]; + latOld = new double[nCells]; + lonOld = new double[nCells]; - xNew = new double[nCellsNew]; - yNew = new double[nCellsNew]; - zNew = new double[nCellsNew]; - latNew = new double[nCellsNew]; - lonNew = new double[nCellsNew]; - idxToNew = new int[nCellsNew]; + xNew = new double[nCellsNew]; + yNew = new double[nCellsNew]; + zNew = new double[nCellsNew]; + latNew = new double[nCellsNew]; + lonNew = new double[nCellsNew]; + idxToNew = new int[nCellsNew]; ncutil::get_var(inputFilename, "xCell", xOld); ncutil::get_var(inputFilename, "yCell", yOld); ncutil::get_var(inputFilename, "zCell", zOld); ncutil::get_var(inputFilename, "latCell", latOld); ncutil::get_var(inputFilename, "lonCell", lonOld); - - idx_map = 0; - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - xNew[idx_map] = xOld[iCell]; - yNew[idx_map] = yOld[iCell]; - zNew[idx_map] = zOld[iCell]; - latNew[idx_map] = latOld[iCell]; - lonNew[idx_map] = lonOld[iCell]; - idxToNew[idx_map] = idx_map+1; - idx_map++; - } - } - - ncutil::def_var(outputFilename, "latCell", + + idx_map = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + xNew[idx_map] = xOld[iCell]; + yNew[idx_map] = yOld[iCell]; + zNew[idx_map] = zOld[iCell]; + latNew[idx_map] = latOld[iCell]; + lonNew[idx_map] = lonOld[iCell]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latCell", NC_DOUBLE, "latitudes of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "lonCell", + ncutil::def_var(outputFilename, "lonCell", NC_DOUBLE, "longitudes of cell centres", {"nCells"}); - ncutil::put_var(outputFilename, "latCell", &latNew[0]); + ncutil::put_var(outputFilename, "latCell", &latNew[0]); ncutil::put_var(outputFilename, "lonCell", &lonNew[0]); - ncutil::def_var(outputFilename, "xCell", + ncutil::def_var(outputFilename, "xCell", NC_DOUBLE, "x-coordinates of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "yCell", + ncutil::def_var(outputFilename, "yCell", NC_DOUBLE, "y-coordinates of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "zCell", + ncutil::def_var(outputFilename, "zCell", NC_DOUBLE, "z-coordinates of cell centres", {"nCells"}); - ncutil::put_var(outputFilename, "xCell", &xNew[0]); + ncutil::put_var(outputFilename, "xCell", &xNew[0]); ncutil::put_var(outputFilename, "yCell", &yNew[0]); ncutil::put_var(outputFilename, "zCell", &zNew[0]); - ncutil::def_var(outputFilename, "indexToCellID", + ncutil::def_var(outputFilename, "indexToCellID", NC_INT, "index to cell ID mapping", {"nCells"}); ncutil::put_var(outputFilename, "indexToCellID", &idxToNew[0]); - delete[] xOld; - delete[] yOld; - delete[] zOld; - delete[] latOld; - delete[] lonOld; - - delete[] xNew; - delete[] yNew; - delete[] zNew; - delete[] latNew; - delete[] lonNew; - delete[] idxToNew; - - //Build and write edge coordinate arrays - xOld = new double[nEdges]; - yOld = new double[nEdges]; - zOld = new double[nEdges]; - latOld = new double[nEdges]; - lonOld = new double[nEdges]; - - xNew = new double[nEdgesNew]; - yNew = new double[nEdgesNew]; - zNew = new double[nEdgesNew]; - latNew = new double[nEdgesNew]; - lonNew = new double[nEdgesNew]; - idxToNew = new int[nEdgesNew]; - - ncutil::get_var(inputFilename, "xEdge", xOld); + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; + + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; + + //Build and write edge coordinate arrays + xOld = new double[nEdges]; + yOld = new double[nEdges]; + zOld = new double[nEdges]; + latOld = new double[nEdges]; + lonOld = new double[nEdges]; + + xNew = new double[nEdgesNew]; + yNew = new double[nEdgesNew]; + zNew = new double[nEdgesNew]; + latNew = new double[nEdgesNew]; + lonNew = new double[nEdgesNew]; + idxToNew = new int[nEdgesNew]; + + ncutil::get_var(inputFilename, "xEdge", xOld); ncutil::get_var(inputFilename, "yEdge", yOld); ncutil::get_var(inputFilename, "zEdge", zOld); ncutil::get_var(inputFilename, "latEdge", latOld); ncutil::get_var(inputFilename, "lonEdge", lonOld); - idx_map = 0; - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - if(edgeMap.at(iEdge) != -1){ - xNew[idx_map] = xOld[iEdge]; - yNew[idx_map] = yOld[iEdge]; - zNew[idx_map] = zOld[iEdge]; - latNew[idx_map] = latOld[iEdge]; - lonNew[idx_map] = lonOld[iEdge]; - idxToNew[idx_map] = idx_map+1; - idx_map++; - } - } - - ncutil::def_var(outputFilename, "latEdge", + idx_map = 0; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + xNew[idx_map] = xOld[iEdge]; + yNew[idx_map] = yOld[iEdge]; + zNew[idx_map] = zOld[iEdge]; + latNew[idx_map] = latOld[iEdge]; + lonNew[idx_map] = lonOld[iEdge]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latEdge", NC_DOUBLE, "latitudes of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "lonEdge", + ncutil::def_var(outputFilename, "lonEdge", NC_DOUBLE, "longitudes of edge centres", {"nEdges"}); - ncutil::put_var(outputFilename, "latEdge", &latNew[0]); + ncutil::put_var(outputFilename, "latEdge", &latNew[0]); ncutil::put_var(outputFilename, "lonEdge", &lonNew[0]); - ncutil::def_var(outputFilename, "xEdge", + ncutil::def_var(outputFilename, "xEdge", NC_DOUBLE, "x-coordinates of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "yEdge", + ncutil::def_var(outputFilename, "yEdge", NC_DOUBLE, "y-coordinates of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "zEdge", + ncutil::def_var(outputFilename, "zEdge", NC_DOUBLE, "z-coordinates of edge centres", {"nEdges"}); - ncutil::put_var(outputFilename, "xEdge", &xNew[0]); + ncutil::put_var(outputFilename, "xEdge", &xNew[0]); ncutil::put_var(outputFilename, "yEdge", &yNew[0]); ncutil::put_var(outputFilename, "zEdge", &zNew[0]); - ncutil::def_var(outputFilename, "indexToEdgeID", + ncutil::def_var(outputFilename, "indexToEdgeID", NC_INT, "index to edge ID mapping", {"nEdges"}); ncutil::put_var(outputFilename, "indexToEdgeID", &idxToNew[0]); - delete[] xOld; - delete[] yOld; - delete[] zOld; - delete[] latOld; - delete[] lonOld; - - delete[] xNew; - delete[] yNew; - delete[] zNew; - delete[] latNew; - delete[] lonNew; - delete[] idxToNew; - - //Build and write vertex coordinate arrays - xOld = new double[nVertices]; - yOld = new double[nVertices]; - zOld = new double[nVertices]; - latOld = new double[nVertices]; - lonOld = new double[nVertices]; - - xNew = new double[nVerticesNew]; - yNew = new double[nVerticesNew]; - zNew = new double[nVerticesNew]; - latNew = new double[nVerticesNew]; - lonNew = new double[nVerticesNew]; - idxToNew = new int[nVerticesNew]; - - ncutil::get_var(inputFilename, "xVertex", xOld); + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; + + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; + + //Build and write vertex coordinate arrays + xOld = new double[nVertices]; + yOld = new double[nVertices]; + zOld = new double[nVertices]; + latOld = new double[nVertices]; + lonOld = new double[nVertices]; + + xNew = new double[nVerticesNew]; + yNew = new double[nVerticesNew]; + zNew = new double[nVerticesNew]; + latNew = new double[nVerticesNew]; + lonNew = new double[nVerticesNew]; + idxToNew = new int[nVerticesNew]; + + ncutil::get_var(inputFilename, "xVertex", xOld); ncutil::get_var(inputFilename, "yVertex", yOld); ncutil::get_var(inputFilename, "zVertex", zOld); ncutil::get_var(inputFilename, "latVertex", latOld); ncutil::get_var(inputFilename, "lonVertex", lonOld); - idx_map = 0; - for(int iVertex = 0; iVertex < nVertices; iVertex++){ - if(vertexMap.at(iVertex) != -1){ - xNew[idx_map] = xOld[iVertex]; - yNew[idx_map] = yOld[iVertex]; - zNew[idx_map] = zOld[iVertex]; - latNew[idx_map] = latOld[iVertex]; - lonNew[idx_map] = lonOld[iVertex]; - idxToNew[idx_map] = idx_map+1; - idx_map++; - } - } - - ncutil::def_var(outputFilename, "latVertex", + idx_map = 0; + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + if(vertexMap.at(iVertex) != -1){ + xNew[idx_map] = xOld[iVertex]; + yNew[idx_map] = yOld[iVertex]; + zNew[idx_map] = zOld[iVertex]; + latNew[idx_map] = latOld[iVertex]; + lonNew[idx_map] = lonOld[iVertex]; + idxToNew[idx_map] = idx_map+1; + idx_map++; + } + } + + ncutil::def_var(outputFilename, "latVertex", NC_DOUBLE, "latitudes of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "lonVertex", + ncutil::def_var(outputFilename, "lonVertex", NC_DOUBLE, "longitudes of vertices", {"nVertices"}); ncutil::put_var(outputFilename, "latVertex", &latNew[0]); ncutil::put_var(outputFilename, "lonVertex", &lonNew[0]); - ncutil::def_var(outputFilename, "xVertex", + ncutil::def_var(outputFilename, "xVertex", NC_DOUBLE, "x-coordinates of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "yVertex", + ncutil::def_var(outputFilename, "yVertex", NC_DOUBLE, "y-coordinates of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "zVertex", + ncutil::def_var(outputFilename, "zVertex", NC_DOUBLE, "z-coordinates of vertices", {"nVertices"}); - ncutil::put_var(outputFilename, "xVertex", &xNew[0]); + ncutil::put_var(outputFilename, "xVertex", &xNew[0]); ncutil::put_var(outputFilename, "yVertex", &yNew[0]); ncutil::put_var(outputFilename, "zVertex", &zNew[0]); - ncutil::def_var(outputFilename, "indexToVertexID", + ncutil::def_var(outputFilename, "indexToVertexID", NC_INT, "index to vertex ID mapping", {"nVertices"}); ncutil::put_var(outputFilename, "indexToVertexID", &idxToNew[0]); - delete[] xOld; - delete[] yOld; - delete[] zOld; - delete[] latOld; - delete[] lonOld; + delete[] xOld; + delete[] yOld; + delete[] zOld; + delete[] latOld; + delete[] lonOld; - delete[] xNew; - delete[] yNew; - delete[] zNew; - delete[] latNew; - delete[] lonNew; - delete[] idxToNew; + delete[] xNew; + delete[] yNew; + delete[] zNew; + delete[] latNew; + delete[] lonNew; + delete[] idxToNew; - return 0; + return 0; }/*}}}*/ -int mapAndOutputCellFields( const string inputFilename, const string outputPath, +int mapAndOutputCellFields( const string inputFilename, const string outputPath, const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function maps and writes all of the cell related fields. Including - * cellsOnCell - * edgesOnCell - * verticesOnCell - * nEdgesonCell - * areaCell - * meshDensity - * - * It also writes the graph.info file which can be used to decompose the mesh. - * - * ***************************************************************/ - - size_t nCellsNew, nEdgesNew, maxEdgesNew, edgeCount; + /***************************************************************** + * + * This function maps and writes all of the cell related fields. Including + * cellsOnCell + * edgesOnCell + * verticesOnCell + * nEdgesonCell + * areaCell + * meshDensity + * + * It also writes the graph.info file which can be used to decompose the mesh. + * + * ***************************************************************/ + + size_t nCellsNew, nEdgesNew, maxEdgesNew, edgeCount; ncutil::get_dim(outputFilename, "nCells", nCellsNew); ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); - double *meshDensityOld, *meshDensityNew; - double *areaCellNew; - int *tmp_arr_old, *nEdgesOnCellOld, *nEdgesOnCellNew; - int *tmp_arr_new; + double *meshDensityOld, *meshDensityNew; + double *areaCellNew; + int *tmp_arr_old, *nEdgesOnCellOld, *nEdgesOnCellNew; + int *tmp_arr_new; - tmp_arr_old = new int[nCells*maxEdges]; - nEdgesOnCellOld = new int[nCells]; - nEdgesOnCellNew = new int[nCellsNew]; + tmp_arr_old = new int[nCells*maxEdges]; + nEdgesOnCellOld = new int[nCells]; + nEdgesOnCellNew = new int[nCellsNew]; ncutil::get_var(inputFilename, "edgesOnCell", tmp_arr_old); ncutil::get_var(inputFilename, "nEdgesOnCell", nEdgesOnCellOld); - // Need to map nEdgesOnCell to get maxEdges - maxEdgesNew = 0; - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - nEdgesOnCellNew[cellMap.at(iCell)] = nEdgesOnCellOld[iCell]; - maxEdgesNew = max(maxEdgesNew, (size_t)nEdgesOnCellNew[cellMap.at(iCell)]); - } - } - tmp_arr_new = new int[nCells * maxEdgesNew]; + // Need to map nEdgesOnCell to get maxEdges + maxEdgesNew = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + nEdgesOnCellNew[cellMap.at(iCell)] = nEdgesOnCellOld[iCell]; + maxEdgesNew = max(maxEdgesNew, (size_t)nEdgesOnCellNew[cellMap.at(iCell)]); + } + } + tmp_arr_new = new int[nCells * maxEdgesNew]; // Write maxEdges and maxEdges2 to output file ncutil::def_dim(outputFilename, "maxEdges", maxEdgesNew); ncutil::def_dim(outputFilename, "maxEdges2", maxEdgesNew * 2); - // Write nEdgesOncell to output file - ncutil::def_var(outputFilename, "nEdgesOnCell", + // Write nEdgesOncell to output file + ncutil::def_var(outputFilename, "nEdgesOnCell", NC_INT, "number of edges on each cell", {"nCells"}); ncutil::put_var(outputFilename, "nEdgesOnCell", &nEdgesOnCellNew[0]); - // Map edgesOnCell - for(int iCell = 0; iCell < nCells; iCell++){ + // Map edgesOnCell + for(int iCell = 0; iCell < nCells; iCell++){ #ifdef _DEBUG - cout << "On cell: " << iCell << endl; + cout << "On cell: " << iCell << endl; #endif - if(cellMap.at(iCell) != -1){ - for(int j = 0; j < maxEdgesNew; j++){ - int iEdge = tmp_arr_old[iCell*maxEdges + j] - 1; + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < maxEdgesNew; j++){ + int iEdge = tmp_arr_old[iCell*maxEdges + j] - 1; - if(iEdge != -1 && iEdge < edgeMap.size()){ - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = edgeMap.at(iEdge)+1; - } else { - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; - } + if(iEdge != -1 && iEdge < edgeMap.size()){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = edgeMap.at(iEdge)+1; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } #ifdef _DEBUG - cout << " Mapping edge: " << iEdge << " to " << - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] << " dbg info: " << + cout << " Mapping edge: " << iEdge << " to " << + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] << " dbg info: " << tmp_arr_old[iCell*maxEdges + j] << " " << j << endl; #endif - } - } - } + } + } + } - ncutil::def_var(outputFilename, "edgesOnCell", + ncutil::def_var(outputFilename, "edgesOnCell", NC_INT, "edges on each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "edgesOnCell", &tmp_arr_new[0]); ncutil::get_var(inputFilename, "cellsOnCell", tmp_arr_old); - // Map cellsOnCell, and determine number of edges in graph. - edgeCount = 0; - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - for(int j = 0; j < nEdgesOnCellOld[iCell]; j++){ - int coc = tmp_arr_old[iCell*maxEdges + j] - 1; - - if(coc != -1 && coc < nCells && cellMap.at(coc) < nCellsNew && cellMap.at(coc) != -1){ - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = cellMap.at(coc)+1; - edgeCount++; - } else { - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; - } - } - } - } - edgeCount = edgeCount / 2; - - // Build graph.info file - ofstream graph(path_join(outputPath, "culled_graph.info")); - graph << nCellsNew << " " << edgeCount << endl; - for(int iCell = 0; iCell < nCellsNew; iCell++){ - for(int j = 0; j < nEdgesOnCellNew[iCell]; j++){ - if (tmp_arr_new[iCell * maxEdgesNew + j] != 0) { - graph << tmp_arr_new[iCell * maxEdgesNew + j] << " "; - } - } - graph << endl; - } - graph.close(); - - ncutil::def_var(outputFilename, "cellsOnCell", + // Map cellsOnCell, and determine number of edges in graph. + edgeCount = 0; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < nEdgesOnCellOld[iCell]; j++){ + int coc = tmp_arr_old[iCell*maxEdges + j] - 1; + + if(coc != -1 && coc < nCells && cellMap.at(coc) < nCellsNew && cellMap.at(coc) != -1){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = cellMap.at(coc)+1; + edgeCount++; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } + } + } + } + edgeCount = edgeCount / 2; + + // Build graph.info file + ofstream graph(path_join(outputPath, "culled_graph.info")); + graph << nCellsNew << " " << edgeCount << endl; + for(int iCell = 0; iCell < nCellsNew; iCell++){ + for(int j = 0; j < nEdgesOnCellNew[iCell]; j++){ + if (tmp_arr_new[iCell * maxEdgesNew + j] != 0) { + graph << tmp_arr_new[iCell * maxEdgesNew + j] << " "; + } + } + graph << endl; + } + graph.close(); + + ncutil::def_var(outputFilename, "cellsOnCell", NC_INT, "cells adj. to each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "cellsOnCell", &tmp_arr_new[0]); - delete[] nEdgesOnCellNew; - delete[] nEdgesOnCellOld; + delete[] nEdgesOnCellNew; + delete[] nEdgesOnCellOld; ncutil::get_var(inputFilename, "verticesOnCell", tmp_arr_old); - // Map verticesOnCell - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - for(int j = 0; j < maxEdgesNew; j++){ - int iVertex = tmp_arr_old[iCell*maxEdges + j] - 1; - - if(iVertex != -1 && iVertex < vertexMap.size()){ - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = vertexMap.at(iVertex)+1; - } else { - tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; - } - } - } - } - - ncutil::def_var(outputFilename, "verticesOnCell", + // Map verticesOnCell + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + for(int j = 0; j < maxEdgesNew; j++){ + int iVertex = tmp_arr_old[iCell*maxEdges + j] - 1; + + if(iVertex != -1 && iVertex < vertexMap.size()){ + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = vertexMap.at(iVertex)+1; + } else { + tmp_arr_new[cellMap.at(iCell)*maxEdgesNew + j] = 0; + } + } + } + } + + ncutil::def_var(outputFilename, "verticesOnCell", NC_INT, "vertices on each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "verticesOnCell", &tmp_arr_new[0]); - delete[] tmp_arr_old; - delete[] tmp_arr_new; + delete[] tmp_arr_old; + delete[] tmp_arr_new; - // Map areaCell - areaCellNew = new double[nCellsNew]; + // Map areaCell + areaCellNew = new double[nCellsNew]; - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - areaCellNew[cellMap.at(iCell)] = areaCell.at(iCell); - } - } + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + areaCellNew[cellMap.at(iCell)] = areaCell.at(iCell); + } + } - ncutil::def_var(outputFilename, "areaCell", + ncutil::def_var(outputFilename, "areaCell", NC_DOUBLE, "surface area of each cell", {"nCells"}); ncutil::put_var(outputFilename, "areaCell", &areaCellNew[0]); - delete[] areaCellNew; + delete[] areaCellNew; - // Map meshDensity - meshDensityOld = new double[nCells]; - meshDensityNew = new double[nCellsNew]; + // Map meshDensity + meshDensityOld = new double[nCells]; + meshDensityNew = new double[nCellsNew]; ncutil::get_var(inputFilename, "meshDensity", meshDensityOld); - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - meshDensityNew[cellMap.at(iCell)] = meshDensityOld[iCell]; - } - } + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + meshDensityNew[cellMap.at(iCell)] = meshDensityOld[iCell]; + } + } - ncutil::def_var(outputFilename, "meshDensity", + ncutil::def_var(outputFilename, "meshDensity", NC_DOUBLE, "mesh density distribution", {"nCells"}); ncutil::put_var(outputFilename, "meshDensity", &meshDensityNew[0]); - return 0; + return 0; }/*}}}*/ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function maps and writes all of the edge related fields. Including - * cellsOnEdge - * edgesOnEdge - * verticesOnEdge - * nEdgesOnEdge - * weightsOnEdge - * dvEdge - * dcEdge - * angleEdge - * - * ***************************************************************/ - - size_t nEdgesNew, maxEdges2New, two = 2; + /***************************************************************** + * + * This function maps and writes all of the edge related fields. Including + * cellsOnEdge + * edgesOnEdge + * verticesOnEdge + * nEdgesOnEdge + * weightsOnEdge + * dvEdge + * dcEdge + * angleEdge + * + * ***************************************************************/ + + size_t nEdgesNew, maxEdges2New, two = 2; ncutil::get_dim(outputFilename, "nEdges", nEdgesNew); ncutil::get_dim(outputFilename, "maxEdges2", maxEdges2New); - int *nEdgesOnEdgeOld, *nEdgesOnEdgeNew; - int *edgesOnEdgeOld, *edgesOnEdgeNew; - int *cellsOnEdgeOld, *cellsOnEdgeNew; - int *verticesOnEdgeOld, *verticesOnEdgeNew; - double *weightsOnEdgeOld, *weightsOnEdgeNew; - double *dvEdgeOld, *dvEdgeNew; - double *dcEdgeOld, *dcEdgeNew; - double *angleEdgeOld, *angleEdgeNew; - - // Need to map cellsOnEdge and verticesOnEdge - cellsOnEdgeOld = new int[nEdges*2]; - cellsOnEdgeNew = new int[nEdgesNew*2]; - verticesOnEdgeOld = new int[nEdges*2]; - verticesOnEdgeNew = new int[nEdgesNew*2]; + int *nEdgesOnEdgeOld, *nEdgesOnEdgeNew; + int *edgesOnEdgeOld, *edgesOnEdgeNew; + int *cellsOnEdgeOld, *cellsOnEdgeNew; + int *verticesOnEdgeOld, *verticesOnEdgeNew; + double *weightsOnEdgeOld, *weightsOnEdgeNew; + double *dvEdgeOld, *dvEdgeNew; + double *dcEdgeOld, *dcEdgeNew; + double *angleEdgeOld, *angleEdgeNew; + + // Need to map cellsOnEdge and verticesOnEdge + cellsOnEdgeOld = new int[nEdges*2]; + cellsOnEdgeNew = new int[nEdgesNew*2]; + verticesOnEdgeOld = new int[nEdges*2]; + verticesOnEdgeNew = new int[nEdgesNew*2]; ncutil::get_var(inputFilename, "cellsOnEdge", cellsOnEdgeOld); ncutil::get_var(inputFilename, "verticesOnEdge", verticesOnEdgeOld); - // Map cellsOnEdge and verticesOnEdge - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - if(edgeMap.at(iEdge) != -1){ - int cell1, cell2; - int vertex1, vertex2; + // Map cellsOnEdge and verticesOnEdge + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + int cell1, cell2; + int vertex1, vertex2; - cell1 = cellsOnEdgeOld[iEdge * 2] - 1; - cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; - vertex1 = verticesOnEdgeOld[iEdge * 2] - 1; - vertex2 = verticesOnEdgeOld[iEdge * 2 + 1] - 1; + cell1 = cellsOnEdgeOld[iEdge * 2] - 1; + cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; + vertex1 = verticesOnEdgeOld[iEdge * 2] - 1; + vertex2 = verticesOnEdgeOld[iEdge * 2 + 1] - 1; #ifdef _DEBUG - cout << "Defining edge: " << endl; - cout << " Old cell1: " << cell1 << endl; - cout << " Old cell2: " << cell2 << endl; - cout << " Old vertex1: " << vertex1 << endl; - cout << " Old vertex2: " << vertex2 << endl; + cout << "Defining edge: " << endl; + cout << " Old cell1: " << cell1 << endl; + cout << " Old cell2: " << cell2 << endl; + cout << " Old vertex1: " << vertex1 << endl; + cout << " Old vertex2: " << vertex2 << endl; #endif - if(cell1 != -1 && cell2 != -1){ - if(cellMap.at(cell1) != -1 && cellMap.at(cell2) != -1){ - cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; - cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = cellMap.at(cell2) + 1; - - verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; - verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; - } else if (cellMap.at(cell2) == -1){ - cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; - cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; - - verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; - verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; - } else if (cellMap.at(cell1) == -1){ - cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; - cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; - - verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; - verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; - } - } else if(cell2 == -1){ - if(cellMap.at(cell1) != -1){ - cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; - cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; - - verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; - verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; - } else { - cout << "ERROR: Edge mask is 1, but has no cells." << endl; - } - } else if(cell1 == -1){ - cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; - cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; - - verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; - verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; - } else { - cout << "ERROR: Edge mask is 1, but has no cells." << endl; - } + if(cell1 != -1 && cell2 != -1){ + if(cellMap.at(cell1) != -1 && cellMap.at(cell2) != -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = cellMap.at(cell2) + 1; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else if (cellMap.at(cell2) == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else if (cellMap.at(cell1) == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; + } + } else if(cell2 == -1){ + if(cellMap.at(cell1) != -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell1) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex1) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex2) + 1; + } else { + cout << "ERROR: Edge mask is 1, but has no cells." << endl; + } + } else if(cell1 == -1){ + cellsOnEdgeNew[edgeMap.at(iEdge)*2] = cellMap.at(cell2) + 1; + cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] = 0; + + verticesOnEdgeNew[edgeMap.at(iEdge)*2] = vertexMap.at(vertex2) + 1; + verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] = vertexMap.at(vertex1) + 1; + } else { + cout << "ERROR: Edge mask is 1, but has no cells." << endl; + } #ifdef _DEBUG - cout << " New cell1: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2] << endl; - cout << " New cell2: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; - cout << " New vertex1: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2] << endl; - cout << " New vertex2: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; + cout << " New cell1: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2] << endl; + cout << " New cell2: " << cellsOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; + cout << " New vertex1: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2] << endl; + cout << " New vertex2: " << verticesOnEdgeNew[edgeMap.at(iEdge)*2+1] << endl; #endif - } - } + } + } - ncutil::def_var(outputFilename, "verticesOnEdge", + ncutil::def_var(outputFilename, "verticesOnEdge", NC_INT, "vertices on each edge", {"nEdges", "TWO"}); - ncutil::def_var(outputFilename, "cellsOnEdge", + ncutil::def_var(outputFilename, "cellsOnEdge", NC_INT, "cells adj. to each edge", {"nEdges", "TWO"}); ncutil::put_var(outputFilename, "verticesOnEdge", &verticesOnEdgeNew[0]); ncutil::put_var(outputFilename, "cellsOnEdge", &cellsOnEdgeNew[0]); - // Don't delete cellsOnEdgeOld yet. It's needed to map edgesOnEdge and weightsOnEdge - delete[] cellsOnEdgeNew; - delete[] verticesOnEdgeOld; - delete[] verticesOnEdgeNew; + // Don't delete cellsOnEdgeOld yet. It's needed to map edgesOnEdge and weightsOnEdge + delete[] cellsOnEdgeNew; + delete[] verticesOnEdgeOld; + delete[] verticesOnEdgeNew; - // Map edgesOnEdge, nEdgesOnEdge, and weightsOnEdge - nEdgesOnEdgeOld = new int[nEdges]; - nEdgesOnEdgeNew = new int[nEdges]; - edgesOnEdgeOld = new int[nEdges*maxEdges*2]; - edgesOnEdgeNew = new int[nEdgesNew*maxEdges2New]; - weightsOnEdgeOld = new double[nEdges*maxEdges*2]; - weightsOnEdgeNew = new double[nEdgesNew*maxEdges2New]; + // Map edgesOnEdge, nEdgesOnEdge, and weightsOnEdge + nEdgesOnEdgeOld = new int[nEdges]; + nEdgesOnEdgeNew = new int[nEdges]; + edgesOnEdgeOld = new int[nEdges*maxEdges*2]; + edgesOnEdgeNew = new int[nEdgesNew*maxEdges2New]; + weightsOnEdgeOld = new double[nEdges*maxEdges*2]; + weightsOnEdgeNew = new double[nEdgesNew*maxEdges2New]; ncutil::get_var(inputFilename, "nEdgesOnEdge", nEdgesOnEdgeOld); ncutil::get_var(inputFilename, "edgesOnEdge", edgesOnEdgeOld); ncutil::get_var(inputFilename, "weightsOnEdge", weightsOnEdgeOld); - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - int edgeCount = 0; - if(edgeMap.at(iEdge) != -1){ - int cell1, cell2; + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + int edgeCount = 0; + if(edgeMap.at(iEdge) != -1){ + int cell1, cell2; - cell1 = cellsOnEdgeOld[iEdge * 2] - 1; - cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; + cell1 = cellsOnEdgeOld[iEdge * 2] - 1; + cell2 = cellsOnEdgeOld[iEdge * 2 + 1] - 1; - if(cell1 != -1){ - cell1 == cellMap.at(cell1); - } + if(cell1 != -1){ + cell1 == cellMap.at(cell1); + } - if(cell2 != -1){ - cell2 == cellMap.at(cell1); - } + if(cell2 != -1){ + cell2 == cellMap.at(cell1); + } - if(cell1 != -1 && cell2 != -1){ - for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ - int eoe = edgesOnEdgeOld[iEdge*maxEdges*2 + j] - 1; + if(cell1 != -1 && cell2 != -1){ + for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ + int eoe = edgesOnEdgeOld[iEdge*maxEdges*2 + j] - 1; - if(eoe != -1 && eoe < edgeMap.size()){ - edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = edgeMap.at(eoe) + 1; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = + if(eoe != -1 && eoe < edgeMap.size()){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = edgeMap.at(eoe) + 1; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = weightsOnEdgeOld[iEdge*maxEdges*2 + j]; - edgeCount++; - } else { - edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - } - } - } else if ( cell1 == -1 || cell2 == -1){ - for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ - edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - } - } - - for(int j = edgeCount; j < maxEdges2New; j++){ - edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - } - nEdgesOnEdgeNew[edgeMap.at(iEdge)] = edgeCount; - } - } - - ncutil::def_var(outputFilename, "nEdgesOnEdge", + edgeCount++; + } else { + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + } + } else if ( cell1 == -1 || cell2 == -1){ + for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + } + + for(int j = edgeCount; j < maxEdges2New; j++){ + edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } + nEdgesOnEdgeNew[edgeMap.at(iEdge)] = edgeCount; + } + } + + ncutil::def_var(outputFilename, "nEdgesOnEdge", NC_INT, "number of edges adj. to each edge", {"nEdges"}); - ncutil::def_var(outputFilename, "edgesOnEdge", + ncutil::def_var(outputFilename, "edgesOnEdge", NC_INT, "edges adj. to each edge", {"nEdges", "maxEdges2"}); - + ncutil::put_var(outputFilename, "nEdgesOnEdge", &nEdgesOnEdgeNew[0]); ncutil::put_var(outputFilename, "edgesOnEdge", &edgesOnEdgeNew[0]); - ncutil::def_var(outputFilename, "weightsOnEdge", + ncutil::def_var(outputFilename, "weightsOnEdge", NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); ncutil::put_var(outputFilename, "weightsOnEdge", &weightsOnEdgeNew[0]); - delete[] nEdgesOnEdgeOld; - delete[] cellsOnEdgeOld; - delete[] edgesOnEdgeOld; - delete[] edgesOnEdgeNew; - delete[] weightsOnEdgeOld; - delete[] weightsOnEdgeNew; - - // Map dvEdge, dcEdge, and angleEdge - dvEdgeOld = new double[nEdges]; - dcEdgeOld = new double[nEdges]; - angleEdgeOld = new double[nEdges]; - dvEdgeNew = new double[nEdgesNew]; - dcEdgeNew = new double[nEdgesNew]; - angleEdgeNew = new double[nEdgesNew]; + delete[] nEdgesOnEdgeOld; + delete[] cellsOnEdgeOld; + delete[] edgesOnEdgeOld; + delete[] edgesOnEdgeNew; + delete[] weightsOnEdgeOld; + delete[] weightsOnEdgeNew; + + // Map dvEdge, dcEdge, and angleEdge + dvEdgeOld = new double[nEdges]; + dcEdgeOld = new double[nEdges]; + angleEdgeOld = new double[nEdges]; + dvEdgeNew = new double[nEdgesNew]; + dcEdgeNew = new double[nEdgesNew]; + angleEdgeNew = new double[nEdgesNew]; ncutil::get_var(inputFilename, "dvEdge", dvEdgeOld); ncutil::get_var(inputFilename, "dcEdge", dcEdgeOld); ncutil::get_var(inputFilename, "angleEdge", angleEdgeOld); - for(int iEdge = 0; iEdge < nEdges; iEdge++){ - if(edgeMap.at(iEdge) != -1){ - dvEdgeNew[edgeMap.at(iEdge)] = dvEdgeOld[iEdge]; - dcEdgeNew[edgeMap.at(iEdge)] = dcEdgeOld[iEdge]; - angleEdgeNew[edgeMap.at(iEdge)] = angleEdgeOld[iEdge]; - } - } + for(int iEdge = 0; iEdge < nEdges; iEdge++){ + if(edgeMap.at(iEdge) != -1){ + dvEdgeNew[edgeMap.at(iEdge)] = dvEdgeOld[iEdge]; + dcEdgeNew[edgeMap.at(iEdge)] = dcEdgeOld[iEdge]; + angleEdgeNew[edgeMap.at(iEdge)] = angleEdgeOld[iEdge]; + } + } - ncutil::def_var(outputFilename, "dvEdge", + ncutil::def_var(outputFilename, "dvEdge", NC_DOUBLE, "length of arc between centres", {"nEdges"}); - ncutil::def_var(outputFilename, "dcEdge", + ncutil::def_var(outputFilename, "dcEdge", NC_DOUBLE, "length of arc between centres", {"nEdges"}); - ncutil::def_var(outputFilename, "angleEdge", + ncutil::def_var(outputFilename, "angleEdge", NC_DOUBLE, "angle to edges", {"nEdges"}) ; ncutil::put_var(outputFilename, "dvEdge", &dvEdgeNew[0]); ncutil::put_var(outputFilename, "dcEdge", &dcEdgeNew[0]); ncutil::put_var(outputFilename, "angleEdge", &angleEdgeNew[0]); - delete[] dvEdgeOld; - delete[] dvEdgeNew; - delete[] dcEdgeOld; - delete[] dcEdgeNew; - delete[] angleEdgeOld; - delete[] angleEdgeNew; + delete[] dvEdgeOld; + delete[] dvEdgeNew; + delete[] dcEdgeOld; + delete[] dcEdgeNew; + delete[] angleEdgeOld; + delete[] angleEdgeNew; - return 0; + return 0; }/*}}}*/ int mapAndOutputVertexFields( const string inputFilename, const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function maps and writes all of the vertex related fields. Including - * cellsOnVertex - * edgesOnVertex - * areaTriangle - * kiteAreasOnVertex - * - * ***************************************************************/ - - size_t nVerticesNew; + /***************************************************************** + * + * This function maps and writes all of the vertex related fields. Including + * cellsOnVertex + * edgesOnVertex + * areaTriangle + * kiteAreasOnVertex + * + * ***************************************************************/ + + size_t nVerticesNew; ncutil::get_dim(outputFilename, "nVertices", nVerticesNew); - int *cellsOnVertexOld, *cellsOnVertexNew; - int *edgesOnVertexOld, *edgesOnVertexNew; - double *kiteAreasOnVertexOld, *kiteAreasOnVertexNew; - double *areaTriangleOld, *areaTriangleNew; + int *cellsOnVertexOld, *cellsOnVertexNew; + int *edgesOnVertexOld, *edgesOnVertexNew; + double *kiteAreasOnVertexOld, *kiteAreasOnVertexNew; + double *areaTriangleOld, *areaTriangleNew; - cellsOnVertexOld = new int[nVertices * vertexDegree]; - cellsOnVertexNew = new int[nVerticesNew * vertexDegree]; - edgesOnVertexOld = new int[nVertices * vertexDegree]; - edgesOnVertexNew = new int[nVerticesNew * vertexDegree]; - kiteAreasOnVertexOld = new double[nVertices * vertexDegree]; - kiteAreasOnVertexNew = new double[nVerticesNew * vertexDegree]; - areaTriangleOld = new double[nVertices]; - areaTriangleNew = new double[nVerticesNew]; + cellsOnVertexOld = new int[nVertices * vertexDegree]; + cellsOnVertexNew = new int[nVerticesNew * vertexDegree]; + edgesOnVertexOld = new int[nVertices * vertexDegree]; + edgesOnVertexNew = new int[nVerticesNew * vertexDegree]; + kiteAreasOnVertexOld = new double[nVertices * vertexDegree]; + kiteAreasOnVertexNew = new double[nVerticesNew * vertexDegree]; + areaTriangleOld = new double[nVertices]; + areaTriangleNew = new double[nVerticesNew]; ncutil::get_var(inputFilename, "cellsOnVertex", cellsOnVertexOld); ncutil::get_var(inputFilename, "edgesOnVertex", edgesOnVertexOld); ncutil::get_var(inputFilename, "kiteAreasOnVertex", kiteAreasOnVertexOld); ncutil::get_var(inputFilename, "areaTriangle", areaTriangleOld); - for(int iVertex = 0; iVertex < nVertices; iVertex++){ - double area = 0.0; - if(vertexMap.at(iVertex) != -1){ - for(int j = 0; j < vertexDegree; j++){ - int iCell, iEdge; - - iCell = cellsOnVertexOld[iVertex*vertexDegree + j] - 1; - iEdge = edgesOnVertexOld[iVertex*vertexDegree + j] - 1; - - if(iCell != -1){ - cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = cellMap.at(iCell) + 1; - if(cellMap.at(iCell) == -1){ - kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; - } else { - kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = + for(int iVertex = 0; iVertex < nVertices; iVertex++){ + double area = 0.0; + if(vertexMap.at(iVertex) != -1){ + for(int j = 0; j < vertexDegree; j++){ + int iCell, iEdge; + + iCell = cellsOnVertexOld[iVertex*vertexDegree + j] - 1; + iEdge = edgesOnVertexOld[iVertex*vertexDegree + j] - 1; + + if(iCell != -1){ + cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = cellMap.at(iCell) + 1; + if(cellMap.at(iCell) == -1){ + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; + } else { + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = kiteAreasOnVertexOld[iVertex*vertexDegree + j]; - } - area += kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j]; - } else { - cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; - kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; - } - - if(iEdge != -1){ - edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = edgeMap.at(iEdge) + 1; - } else { - edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; - } - } - - areaTriangleNew[vertexMap.at(iVertex)] = area; - } - } - - ncutil::def_var(outputFilename, "edgesOnVertex", + } + area += kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j]; + } else { + cellsOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; + kiteAreasOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0.0; + } + + if(iEdge != -1){ + edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = edgeMap.at(iEdge) + 1; + } else { + edgesOnVertexNew[ vertexMap.at(iVertex) * vertexDegree + j] = 0; + } + } + + areaTriangleNew[vertexMap.at(iVertex)] = area; + } + } + + ncutil::def_var(outputFilename, "edgesOnVertex", NC_INT, "edges adj. to each vertex", {"nVertices", "vertexDegree"}); - ncutil::def_var(outputFilename, "cellsOnVertex", + ncutil::def_var(outputFilename, "cellsOnVertex", NC_INT, "cells adj. to each vertex", {"nVertices", "vertexDegree"}); - + ncutil::put_var(outputFilename, "edgesOnVertex", &edgesOnVertexNew [0]); ncutil::put_var(outputFilename, "cellsOnVertex", &cellsOnVertexNew [0]); - ncutil::def_var(outputFilename, "areaTriangle", + ncutil::def_var(outputFilename, "areaTriangle", NC_DOUBLE, "surface area of dual cells", {"nVertices"}); - ncutil::def_var(outputFilename, "kiteAreasOnVertex", - NC_DOUBLE, + ncutil::def_var(outputFilename, "kiteAreasOnVertex", + NC_DOUBLE, "surface areas of overlap between cells and dual cells", {"nVertices", "vertexDegree"}); ncutil::put_var(outputFilename, "areaTriangle", &areaTriangleNew [0]); ncutil::put_var(outputFilename, "kiteAreasOnVertex", &kiteAreasOnVertexNew [0]); - delete[] cellsOnVertexOld; - delete[] cellsOnVertexNew; - delete[] edgesOnVertexOld; - delete[] edgesOnVertexNew; - delete[] kiteAreasOnVertexOld; - delete[] kiteAreasOnVertexNew; - delete[] areaTriangleOld; - delete[] areaTriangleNew; + delete[] cellsOnVertexOld; + delete[] cellsOnVertexNew; + delete[] edgesOnVertexOld; + delete[] edgesOnVertexNew; + delete[] kiteAreasOnVertexOld; + delete[] kiteAreasOnVertexNew; + delete[] areaTriangleOld; + delete[] areaTriangleNew; - return 0; + return 0; }/*}}}*/ /*}}}*/ string gen_random(const int len) {/*{{{*/ - static const char alphanum[] = - "0123456789" -// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; + static const char alphanum[] = + "0123456789" +// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; - string rand_str = ""; + string rand_str = ""; - for (int i = 0; i < len; ++i) { - rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; - } + for (int i = 0; i < len; ++i) { + rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; + } - return rand_str; + return rand_str; }/*}}}*/ diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp index ac22939ae..7a590eac4 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp @@ -80,9 +80,9 @@ vector< vector >::iterator vec_dbl_itr; vector::iterator dbl_itr; // }}} struct int_hasher {/*{{{*/ - size_t operator()(const int i) const { - return (size_t)i; - } + size_t operator()(const int i) const { + return (size_t)i; + } };/*}}}*/ /* Building/Ordering functions {{{ */ @@ -117,2281 +117,2281 @@ int writeGraphFile(const string outputFilename); string gen_random(const int len); int main ( int argc, char *argv[] ) { - int error; - string out_name = "mesh.nc"; - string in_name = "grid.nc"; + int error; + string out_name = "mesh.nc"; + string in_name = "grid.nc"; string out_path = ""; string out_file = ""; string out_fext = ""; - cout << endl << endl; - cout << "************************************************************" << endl; - cout << "MPAS_MESH_CONVERTER:\n"; - cout << " C++ version\n"; - cout << " Convert a NetCDF file describing Cell Locations, \n"; - cout << " Vertex Location, and Connectivity into a valid MPAS mesh.\n"; - cout << endl << endl; - cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; - cout << "************************************************************" << endl; - cout << endl << endl; - // - // If the input file was not specified, get it now. - // - if ( argc <= 1 ) - { - cout << "\n"; - cout << "MPAS_MESH_CONVERTER:\n"; - cout << " Please enter the NetCDF input filename.\n"; - - cin >> in_name; - - cout << "\n"; - cout << "MPAS_MESH_CONVERTER:\n"; - cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; - - cin >> out_name; - } - else if (argc == 2) - { - in_name = argv[1]; - - cout << "\n"; - cout << "MPAS_MESH_CONVERTER:\n"; - cout << " Output name not specified. Using default of mesh.nc\n"; - } - else if (argc == 3) - { - in_name = argv[1]; - out_name = argv[2]; - } - - if(in_name == out_name){ - cout << " ERROR: Input and output names are the same." << endl; - return 1; - } + cout << endl << endl; + cout << "************************************************************" << endl; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " C++ version\n"; + cout << " Convert a NetCDF file describing Cell Locations, \n"; + cout << " Vertex Location, and Connectivity into a valid MPAS mesh.\n"; + cout << endl << endl; + cout << " Compiled on " << __DATE__ << " at " << __TIME__ << ".\n"; + cout << "************************************************************" << endl; + cout << endl << endl; + // + // If the input file was not specified, get it now. + // + if ( argc <= 1 ) + { + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Please enter the NetCDF input filename.\n"; + + cin >> in_name; + + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Please enter the output NetCDF MPAS Mesh filename.\n"; + + cin >> out_name; + } + else if (argc == 2) + { + in_name = argv[1]; + + cout << "\n"; + cout << "MPAS_MESH_CONVERTER:\n"; + cout << " Output name not specified. Using default of mesh.nc\n"; + } + else if (argc == 3) + { + in_name = argv[1]; + out_name = argv[2]; + } + + if(in_name == out_name){ + cout << " ERROR: Input and output names are the same." << endl; + return 1; + } file_part(out_name, out_path, out_file, out_fext); - srand(time(NULL)); - - cout << "Reading input grid." << endl; - error = readGridInput(in_name); - if(error) return 1; - - cout << "Build prelimiary cell connectivity." << endl; - error = buildUnorderedCellConnectivity(); - if(error) return 1; - - cout << "Order vertices on cell." << endl; - error = firstOrderingVerticesOnCell(); - if(error) return 1; - - cout << "Build complete cell mask." << endl; - error = buildCompleteCellMask(); - if(error) return 1; - - cout << "Build and order edges, dvEdge, and dcEdge." << endl; - error = buildEdges(); - if(error) return 1; - - cout << "Build and order vertex arrays," << endl; - error = orderVertexArrays(); - if(error) return 1; - - cout << "Build and order cell arrays," << endl; - error = orderCellArrays(); - if(error) return 1; - - cout << "Build areaCell, areaTriangle, and kiteAreasOnVertex." << endl; - error = buildAreas(); - if(error) return 1; - - cout << "Build edgesOnEdge and weightsOnEdge." << endl; - error = buildEdgesOnEdgeArrays(); - if(error) return 1; - - cout << "Build angleEdge." << endl; - error = buildAngleEdge(); - if(error) return 1; - - cout << "Building mesh qualities." << endl; - error = buildMeshQualities(); - if(error) return 1; - - cout << "Writing grid dimensions" << endl; - if(error = outputGridDimensions(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing grid attributes" << endl; - if(error = outputGridAttributes(out_name, in_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing grid coordinates" << endl; - if(error = outputGridCoordinates(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing cell connectivity" << endl; - if(error = outputCellConnectivity(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing edge connectivity" << endl; - if(error = outputEdgeConnectivity(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing vertex connectivity" << endl; - if(error = outputVertexConnectivity(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing cell parameters" << endl; - if(error = outputCellParameters(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing edge parameters" << endl; - if(error = outputEdgeParameters(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - cout << "Writing vertex parameters" << endl; - if(error = outputVertexParameters(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Writing mesh qualities" << endl; - if(error = outputMeshQualities(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Reading and writing meshDensity" << endl; - if(error = outputMeshDensity(out_name)){ - cout << "Error - " << error << endl; - exit(error); - } - - cout << "Write graph.info file" << endl; - if(error = writeGraphFile(path_join(out_path, "graph.info"))){ - cout << "Error - " << error << endl; - exit(error); - } + srand(time(NULL)); + + cout << "Reading input grid." << endl; + error = readGridInput(in_name); + if(error) return 1; + + cout << "Build prelimiary cell connectivity." << endl; + error = buildUnorderedCellConnectivity(); + if(error) return 1; + + cout << "Order vertices on cell." << endl; + error = firstOrderingVerticesOnCell(); + if(error) return 1; + + cout << "Build complete cell mask." << endl; + error = buildCompleteCellMask(); + if(error) return 1; + + cout << "Build and order edges, dvEdge, and dcEdge." << endl; + error = buildEdges(); + if(error) return 1; + + cout << "Build and order vertex arrays," << endl; + error = orderVertexArrays(); + if(error) return 1; + + cout << "Build and order cell arrays," << endl; + error = orderCellArrays(); + if(error) return 1; + + cout << "Build areaCell, areaTriangle, and kiteAreasOnVertex." << endl; + error = buildAreas(); + if(error) return 1; + + cout << "Build edgesOnEdge and weightsOnEdge." << endl; + error = buildEdgesOnEdgeArrays(); + if(error) return 1; + + cout << "Build angleEdge." << endl; + error = buildAngleEdge(); + if(error) return 1; + + cout << "Building mesh qualities." << endl; + error = buildMeshQualities(); + if(error) return 1; + + cout << "Writing grid dimensions" << endl; + if(error = outputGridDimensions(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing grid attributes" << endl; + if(error = outputGridAttributes(out_name, in_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing grid coordinates" << endl; + if(error = outputGridCoordinates(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing cell connectivity" << endl; + if(error = outputCellConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing edge connectivity" << endl; + if(error = outputEdgeConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing vertex connectivity" << endl; + if(error = outputVertexConnectivity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing cell parameters" << endl; + if(error = outputCellParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing edge parameters" << endl; + if(error = outputEdgeParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + cout << "Writing vertex parameters" << endl; + if(error = outputVertexParameters(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Writing mesh qualities" << endl; + if(error = outputMeshQualities(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Reading and writing meshDensity" << endl; + if(error = outputMeshDensity(out_name)){ + cout << "Error - " << error << endl; + exit(error); + } + + cout << "Write graph.info file" << endl; + if(error = writeGraphFile(path_join(out_path, "graph.info"))){ + cout << "Error - " << error << endl; + exit(error); + } } /* Building/Ordering functions {{{ */ int readGridInput(const string inputFilename){/*{{{*/ - int ncid, retv; + int ncid, retv; size_t nCells, nVertices, vertexDegree; string on_a_sphere, is_periodic; - double *xcell, *ycell,*zcell; - double *xvertex, *yvertex,*zvertex; - int *cellsonvertex_list; - pnt new_location; + double *xcell, *ycell,*zcell; + double *xvertex, *yvertex,*zvertex; + int *cellsonvertex_list; + pnt new_location; #ifdef _DEBUG - cout << endl << endl << "Begin function: readGridInput" << endl << endl; + cout << endl << endl << "Begin function: readGridInput" << endl << endl; #endif - // Initialize range mins with huge values. - xCellRange[0] = 1E10; - yCellRange[0] = 1E10; - zCellRange[0] = 1E10; - xVertexRange[0] = 1E10; - yVertexRange[0] = 1E10; - zVertexRange[0] = 1E10; + // Initialize range mins with huge values. + xCellRange[0] = 1E10; + yCellRange[0] = 1E10; + zCellRange[0] = 1E10; + xVertexRange[0] = 1E10; + yVertexRange[0] = 1E10; + zVertexRange[0] = 1E10; - // Initialize range maxes with small values. - xCellRange[1] = -1E10; - yCellRange[1] = -1E10; - zCellRange[1] = -1E10; - xVertexRange[1] = -1E10; - yVertexRange[1] = -1E10; - zVertexRange[1] = -1E10; + // Initialize range maxes with small values. + xCellRange[1] = -1E10; + yCellRange[1] = -1E10; + zCellRange[1] = -1E10; + xVertexRange[1] = -1E10; + yVertexRange[1] = -1E10; + zVertexRange[1] = -1E10; ncutil::get_dim(inputFilename, "nCells", nCells); ncutil::get_dim(inputFilename, "nVertices", nVertices); ncutil::get_dim(inputFilename, "vertexDegree", vertexDegree); - vertex_degree = vertexDegree; + vertex_degree = vertexDegree; try { #ifdef _DEBUG - cout << " Reading on_a_sphere" << endl; + cout << " Reading on_a_sphere" << endl; #endif ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); - spherical = (on_a_sphere.find("YES") != string::npos); + spherical = (on_a_sphere.find("YES") != string::npos); #ifdef _DEBUG - cout << " Reading sphere_radius" << endl; + cout << " Reading sphere_radius" << endl; #endif ncutil::get_att(inputFilename, "sphere_radius", &sphereRadius); #ifdef _DEBUG - cout << " Reading history" << endl; + cout << " Reading history" << endl; #endif ncutil::get_str(inputFilename, "history", in_history); #ifdef _DEBUG - cout << " Reading file_id" << endl; + cout << " Reading file_id" << endl; #endif ncutil::get_str(inputFilename, "file_id", in_file_id); #ifdef _DEBUG - cout << " Reading parent_id" << endl; + cout << " Reading parent_id" << endl; #endif - ncutil::get_str(inputFilename, "parent_id", in_parent_id); + ncutil::get_str(inputFilename, "parent_id", in_parent_id); #ifdef _DEBUG - cout << " Reading is_periodic" << endl; + cout << " Reading is_periodic" << endl; #endif ncutil::get_str(inputFilename, "is_periodic", is_periodic); periodic = (is_periodic.find("YES") != string::npos); #ifdef _DEBUG - cout << " Reading x_period" << endl; + cout << " Reading x_period" << endl; #endif - ncutil::get_att(inputFilename, "x_period", &xPeriod); + ncutil::get_att(inputFilename, "x_period", &xPeriod); #ifdef _DEBUG - cout << " Reading y_period" << endl; + cout << " Reading y_period" << endl; #endif - ncutil::get_att(inputFilename, "y_period", &yPeriod); - + ncutil::get_att(inputFilename, "y_period", &yPeriod); + } catch (...) { - // allow errors for optional attr. not found + // allow errors for optional attr. not found } - cout << "Read dimensions:" << endl; - cout << " nCells = " << nCells << endl; - cout << " nVertices = " << nVertices << endl; - cout << " vertexDegree = " << vertexDegree << endl; - cout << " Spherical? = " << spherical << endl; - cout << " Periodic? = " << periodic << endl; - if ( periodic ) { - cout << " x_period = " << xPeriod << endl; - cout << " y_period = " << yPeriod << endl; - } + cout << "Read dimensions:" << endl; + cout << " nCells = " << nCells << endl; + cout << " nVertices = " << nVertices << endl; + cout << " vertexDegree = " << vertexDegree << endl; + cout << " Spherical? = " << spherical << endl; + cout << " Periodic? = " << periodic << endl; + if ( periodic ) { + cout << " x_period = " << xPeriod << endl; + cout << " y_period = " << yPeriod << endl; + } - // Build cell center location information - xcell = new double[nCells]; - ycell = new double[nCells]; - zcell = new double[nCells]; + // Build cell center location information + xcell = new double[nCells]; + ycell = new double[nCells]; + zcell = new double[nCells]; ncutil::get_var(inputFilename, "xCell", xcell); ncutil::get_var(inputFilename, "yCell", ycell); ncutil::get_var(inputFilename, "zCell", zcell); - cells.clear(); - for(int i = 0; i < nCells; i++){ - xCellRange[0] = min(xCellRange[0], xcell[i]); - xCellRange[1] = max(xCellRange[1], xcell[i]); + cells.clear(); + for(int i = 0; i < nCells; i++){ + xCellRange[0] = min(xCellRange[0], xcell[i]); + xCellRange[1] = max(xCellRange[1], xcell[i]); - yCellRange[0] = min(yCellRange[0], ycell[i]); - yCellRange[1] = max(yCellRange[1], ycell[i]); + yCellRange[0] = min(yCellRange[0], ycell[i]); + yCellRange[1] = max(yCellRange[1], ycell[i]); - zCellRange[0] = min(zCellRange[0], zcell[i]); - zCellRange[1] = max(zCellRange[1], zcell[i]); + zCellRange[0] = min(zCellRange[0], zcell[i]); + zCellRange[1] = max(zCellRange[1], zcell[i]); - new_location = pnt(xcell[i], ycell[i], zcell[i], i); + new_location = pnt(xcell[i], ycell[i], zcell[i], i); - if(spherical) new_location.normalize(); - cells.push_back(new_location); - } + if(spherical) new_location.normalize(); + cells.push_back(new_location); + } - cout << "Built " << cells.size() << " cells." << endl; - delete[] xcell; - delete[] ycell; - delete[] zcell; + cout << "Built " << cells.size() << " cells." << endl; + delete[] xcell; + delete[] ycell; + delete[] zcell; - // Build vertex location information - xvertex = new double[nVertices]; - yvertex = new double[nVertices]; - zvertex = new double[nVertices]; + // Build vertex location information + xvertex = new double[nVertices]; + yvertex = new double[nVertices]; + zvertex = new double[nVertices]; ncutil::get_var(inputFilename, "xVertex", xvertex); ncutil::get_var(inputFilename, "yVertex", yvertex); ncutil::get_var(inputFilename, "zVertex", zvertex); - vertices.clear(); - for(int i = 0; i < nVertices; i++){ - xVertexRange[0] = min(xVertexRange[0], xvertex[i]); - xVertexRange[1] = max(xVertexRange[1], xvertex[i]); + vertices.clear(); + for(int i = 0; i < nVertices; i++){ + xVertexRange[0] = min(xVertexRange[0], xvertex[i]); + xVertexRange[1] = max(xVertexRange[1], xvertex[i]); - yVertexRange[0] = min(yVertexRange[0], yvertex[i]); - yVertexRange[1] = max(yVertexRange[1], yvertex[i]); + yVertexRange[0] = min(yVertexRange[0], yvertex[i]); + yVertexRange[1] = max(yVertexRange[1], yvertex[i]); - zVertexRange[0] = min(zVertexRange[0], zvertex[i]); - zVertexRange[1] = max(zVertexRange[1], zvertex[i]); + zVertexRange[0] = min(zVertexRange[0], zvertex[i]); + zVertexRange[1] = max(zVertexRange[1], zvertex[i]); - new_location = pnt(xvertex[i], yvertex[i], zvertex[i], i); - if(spherical) new_location.normalize(); - vertices.push_back(new_location); - } + new_location = pnt(xvertex[i], yvertex[i], zvertex[i], i); + if(spherical) new_location.normalize(); + vertices.push_back(new_location); + } - cout << "Built " << vertices.size() << " vertices." << endl; - delete[] xvertex; - delete[] yvertex; - delete[] zvertex; + cout << "Built " << vertices.size() << " vertices." << endl; + delete[] xvertex; + delete[] yvertex; + delete[] zvertex; - // Build unordered cellsOnVertex information - cellsonvertex_list = new int[nVertices * vertexDegree]; + // Build unordered cellsOnVertex information + cellsonvertex_list = new int[nVertices * vertexDegree]; - ncutil::get_var(inputFilename, "cellsOnVertex", cellsonvertex_list); - cellsOnVertex.resize(nVertices); + ncutil::get_var(inputFilename, "cellsOnVertex", cellsonvertex_list); + cellsOnVertex.resize(nVertices); - for(int i = 0; i < nVertices; i++){ - for(int j = 0; j < vertexDegree; j++){ - // Subtract 1 to convert into base 0 (c index space). - cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); - } - } + for(int i = 0; i < nVertices; i++){ + for(int j = 0; j < vertexDegree; j++){ + // Subtract 1 to convert into base 0 (c index space). + cellsOnVertex.at(i).push_back(cellsonvertex_list[i*vertexDegree + j] - 1); + } + } - delete[] cellsonvertex_list; + delete[] cellsonvertex_list; - meshDensity.clear(); - meshDensity.resize(cells.size()); + meshDensity.clear(); + meshDensity.resize(cells.size()); ncutil::get_var(inputFilename, "meshDensity", &meshDensity[0]); - - xCellDistance = fabs(xCellRange[1] - xCellRange[0]); - yCellDistance = fabs(yCellRange[1] - yCellRange[0]); - zCellDistance = fabs(zCellRange[1] - zCellRange[0]); - - xVertexDistance = fabs(xVertexRange[1] - xVertexRange[0]); - yVertexDistance = fabs(yVertexRange[1] - yVertexRange[0]); - zVertexDistance = fabs(zVertexRange[1] - zVertexRange[0]); - - if(periodic){ - // Quads are not staggered in the y direction, so need to period by - // max distance + min distance - if(vertexDegree == 4){ - if(xPeriod < 0.0){ - xPeriodicFix = xCellRange[0] + xCellRange[1]; - } else { - xPeriodicFix = xPeriod; - } - - if(yPeriod < 0.0){ - yPeriodicFix = yCellRange[0] + yCellRange[1]; - } else { - yPeriodicFix = yPeriod; - } - // Triangles can be staggered, so only period my max distance - } else { - if(xPeriod < 0.0){ - xPeriodicFix = xCellRange[1]; - } else { - xPeriodicFix = xPeriod; - } - - if(yPeriod < 0.0){ - yPeriodicFix = yCellRange[1]; - } else { - yPeriodicFix = yPeriod; - } - } - } else { - xPeriodicFix = 1.0e5 * abs(xCellRange[1] + xCellRange[0]); - yPeriodicFix = 1.0e5 * abs(yCellRange[1] + yCellRange[0]); - } - -#ifdef _DEBUG - xPeriod = xPeriodicFix; - yPeriod = yPeriodicFix; - cout << "cell Mins: " << xCellRange[0] << " " << yCellRange[0] << " " << zCellRange[0] << endl; - cout << "vertex Mins: " << xVertexRange[0] << " " << yVertexRange[0] << " " << zVertexRange[0] << endl; - cout << "cell Maxes: " << xCellRange[1] << " " << yCellRange[1] << " " << zCellRange[1] << endl; - cout << "vertex Maxes: " << xVertexRange[1] << " " << yVertexRange[1] << " " << zVertexRange[1] << endl; - cout << "cell Distances: " << xCellDistance << " " << yCellDistance << " " << zCellDistance << endl; - cout << "vertex Distances: " << xVertexDistance << " " << yVertexDistance << " " << zVertexDistance << endl; - cout << "xPeriodicFix: " << xPeriodicFix << endl; - cout << "yPeriodicFix: " << yPeriodicFix << endl; - cout << "xPeriod: " << xPeriod << endl; - cout << "yPeriod: " << yPeriod << endl; -#endif - - if(!spherical && (zCellDistance > 0.0 || zVertexDistance > 0.0)){ - cout << "ERROR:" << endl; - cout << " This point set is defined in the plane, but has non-zero Z coordinates." << endl; - cout << endl; - return 1; - } - - return 0; + + xCellDistance = fabs(xCellRange[1] - xCellRange[0]); + yCellDistance = fabs(yCellRange[1] - yCellRange[0]); + zCellDistance = fabs(zCellRange[1] - zCellRange[0]); + + xVertexDistance = fabs(xVertexRange[1] - xVertexRange[0]); + yVertexDistance = fabs(yVertexRange[1] - yVertexRange[0]); + zVertexDistance = fabs(zVertexRange[1] - zVertexRange[0]); + + if(periodic){ + // Quads are not staggered in the y direction, so need to period by + // max distance + min distance + if(vertexDegree == 4){ + if(xPeriod < 0.0){ + xPeriodicFix = xCellRange[0] + xCellRange[1]; + } else { + xPeriodicFix = xPeriod; + } + + if(yPeriod < 0.0){ + yPeriodicFix = yCellRange[0] + yCellRange[1]; + } else { + yPeriodicFix = yPeriod; + } + // Triangles can be staggered, so only period my max distance + } else { + if(xPeriod < 0.0){ + xPeriodicFix = xCellRange[1]; + } else { + xPeriodicFix = xPeriod; + } + + if(yPeriod < 0.0){ + yPeriodicFix = yCellRange[1]; + } else { + yPeriodicFix = yPeriod; + } + } + } else { + xPeriodicFix = 1.0e5 * abs(xCellRange[1] + xCellRange[0]); + yPeriodicFix = 1.0e5 * abs(yCellRange[1] + yCellRange[0]); + } + +#ifdef _DEBUG + xPeriod = xPeriodicFix; + yPeriod = yPeriodicFix; + cout << "cell Mins: " << xCellRange[0] << " " << yCellRange[0] << " " << zCellRange[0] << endl; + cout << "vertex Mins: " << xVertexRange[0] << " " << yVertexRange[0] << " " << zVertexRange[0] << endl; + cout << "cell Maxes: " << xCellRange[1] << " " << yCellRange[1] << " " << zCellRange[1] << endl; + cout << "vertex Maxes: " << xVertexRange[1] << " " << yVertexRange[1] << " " << zVertexRange[1] << endl; + cout << "cell Distances: " << xCellDistance << " " << yCellDistance << " " << zCellDistance << endl; + cout << "vertex Distances: " << xVertexDistance << " " << yVertexDistance << " " << zVertexDistance << endl; + cout << "xPeriodicFix: " << xPeriodicFix << endl; + cout << "yPeriodicFix: " << yPeriodicFix << endl; + cout << "xPeriod: " << xPeriod << endl; + cout << "yPeriod: " << yPeriod << endl; +#endif + + if(!spherical && (zCellDistance > 0.0 || zVertexDistance > 0.0)){ + cout << "ERROR:" << endl; + cout << " This point set is defined in the plane, but has non-zero Z coordinates." << endl; + cout << endl; + return 1; + } + + return 0; }/*}}}*/ int buildUnorderedCellConnectivity(){/*{{{*/ - // buildUnorderedCellConnectivity should assume that cellsOnVertex hasn't been ordered properly yet. - // It should compute the inverse of cellsOnVertex (verticesOnCell) unordered. - // It will also compute an unordered cellsOnCell that can be considered invalid for actual use (e.g. quad grids). - // This ordering should happen regardless of it the mesh is planar or spherical. - - int iVertex, iCell, newCell, j, k, l, m, matches; - bool add; - -#ifdef _DEBUG - cout << endl << endl << "Begin function: buildUnorderedCellConnectivity" << endl << endl; -#endif - - verticesOnCell.clear(); - verticesOnCell.resize(cells.size()); - - for(iVertex = 0; iVertex < vertices.size(); iVertex++){ - for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ - iCell = cellsOnVertex.at(iVertex).at(j); - if(iCell != -1){ - add = true; - for(int k = 0; k < verticesOnCell.at(iCell).size(); k++){ - if(verticesOnCell.at(iCell).at(k) == iVertex){ - add = false; - } - } - - if(add) { - verticesOnCell.at(iCell).push_back(iVertex); - } - } - } - } - - - cellsOnCell.clear(); - cellsOnCell.resize(cells.size()); - for(iCell = 0; iCell < cells.size(); iCell++){ - for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ - iVertex = verticesOnCell.at(iCell).at(j); - for(k = 0; k < cellsOnVertex.at(iVertex).size(); k++){ - newCell = cellsOnVertex.at(iVertex).at(k); - - if(newCell != iCell){ - add = true; - for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ - if(cellsOnCell.at(iCell).at(l) == newCell){ - add = false; - } - } - - if(add) { - if(newCell != -1){ - matches = 0; - for(l = 0; l < verticesOnCell.at(iCell).size(); l++){ - for(m = 0; m < verticesOnCell.at(newCell).size(); m++){ - if(verticesOnCell.at(iCell).at(l) == verticesOnCell.at(newCell).at(m)){ - matches++; - } - } - } - - if(matches == 2){ - cellsOnCell.at(iCell).push_back(newCell); -#ifdef _DEBUG - cout << " Found two shared vertices for cell edge. Adding cell." << endl; -#endif - } else { -#ifdef _DEBUG - cout << " Only found " << matches << + // buildUnorderedCellConnectivity should assume that cellsOnVertex hasn't been ordered properly yet. + // It should compute the inverse of cellsOnVertex (verticesOnCell) unordered. + // It will also compute an unordered cellsOnCell that can be considered invalid for actual use (e.g. quad grids). + // This ordering should happen regardless of it the mesh is planar or spherical. + + int iVertex, iCell, newCell, j, k, l, m, matches; + bool add; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildUnorderedCellConnectivity" << endl << endl; +#endif + + verticesOnCell.clear(); + verticesOnCell.resize(cells.size()); + + for(iVertex = 0; iVertex < vertices.size(); iVertex++){ + for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + iCell = cellsOnVertex.at(iVertex).at(j); + if(iCell != -1){ + add = true; + for(int k = 0; k < verticesOnCell.at(iCell).size(); k++){ + if(verticesOnCell.at(iCell).at(k) == iVertex){ + add = false; + } + } + + if(add) { + verticesOnCell.at(iCell).push_back(iVertex); + } + } + } + } + + + cellsOnCell.clear(); + cellsOnCell.resize(cells.size()); + for(iCell = 0; iCell < cells.size(); iCell++){ + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + iVertex = verticesOnCell.at(iCell).at(j); + for(k = 0; k < cellsOnVertex.at(iVertex).size(); k++){ + newCell = cellsOnVertex.at(iVertex).at(k); + + if(newCell != iCell){ + add = true; + for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ + if(cellsOnCell.at(iCell).at(l) == newCell){ + add = false; + } + } + + if(add) { + if(newCell != -1){ + matches = 0; + for(l = 0; l < verticesOnCell.at(iCell).size(); l++){ + for(m = 0; m < verticesOnCell.at(newCell).size(); m++){ + if(verticesOnCell.at(iCell).at(l) == verticesOnCell.at(newCell).at(m)){ + matches++; + } + } + } + + if(matches == 2){ + cellsOnCell.at(iCell).push_back(newCell); +#ifdef _DEBUG + cout << " Found two shared vertices for cell edge. Adding cell." << endl; +#endif + } else { +#ifdef _DEBUG + cout << " Only found " << matches << " shared vertices for cell edge. Not adding cell." << endl; #endif - } - } else { - cellsOnCell.at(iCell).push_back(newCell); - } - } - } - } - } - } + } + } else { + cellsOnCell.at(iCell).push_back(newCell); + } + } + } + } + } + } - return 0; + return 0; }/*}}}*/ int firstOrderingVerticesOnCell(){/*{{{*/ - /* - * firstOrderingVerticesOnCell should order the vertices around a cell such that they are connected. - * i.e. verticesOnCell.at(iCell).at(i) should be the tail of a vector pointing to - * verticesOnCell.at(iCell).at(i+1) - * - * This is done by computing angles between vectors pointing from the cell center - * to each individual vertex. The next vertex in the list, has the smallest positive angle. - * - */ + /* + * firstOrderingVerticesOnCell should order the vertices around a cell such that they are connected. + * i.e. verticesOnCell.at(iCell).at(i) should be the tail of a vector pointing to + * verticesOnCell.at(iCell).at(i+1) + * + * This is done by computing angles between vectors pointing from the cell center + * to each individual vertex. The next vertex in the list, has the smallest positive angle. + * + */ - pnt vec1, vec2, cross, normal; - pnt vertex1, vertex2; - int iCell, iVertex1, iVertex2, j, k, l, swp_idx, swp; - double dot, mag1, mag2, angle, min_angle; + pnt vec1, vec2, cross, normal; + pnt vertex1, vertex2; + int iCell, iVertex1, iVertex2, j, k, l, swp_idx, swp; + double dot, mag1, mag2, angle, min_angle; #ifdef _DEBUG - cout << endl << endl << "Begin function: firstOrderingVerticesOnCell" << endl << endl; + cout << endl << endl << "Begin function: firstOrderingVerticesOnCell" << endl << endl; #endif - if(!spherical){ - normal = pnt(0.0, 0.0, 1.0); - } + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } - for(iCell = 0; iCell < cells.size(); iCell++){ + for(iCell = 0; iCell < cells.size(); iCell++){ #ifdef _DEBUG - cout << "new cell: " << iCell << endl; - cout << " " << cells.at(iCell) << endl; + cout << "new cell: " << iCell << endl; + cout << " " << cells.at(iCell) << endl; #endif - if(spherical){ - normal = cells.at(iCell); - } + if(spherical){ + normal = cells.at(iCell); + } #ifdef _DEBUG - cout << " Unsorted verticesOnCell: "; - for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ - cout << verticesOnCell.at(iCell).at(j) << " "; - } - cout << endl; - for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ - cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; - for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ - cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; - } - cout << endl; - } + cout << " Unsorted verticesOnCell: "; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << verticesOnCell.at(iCell).at(j) << " "; + } + cout << endl; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; + for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ + cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; + } + cout << endl; + } #endif - // Loop over all vertices except the last two as a starting vertex - for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ - iVertex1 = verticesOnCell.at(iCell).at(j); + // Loop over all vertices except the last two as a starting vertex + for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ + iVertex1 = verticesOnCell.at(iCell).at(j); - vertex1 = vertices.at(iVertex1); + vertex1 = vertices.at(iVertex1); - //Check for, and fix up periodicity - if(!spherical){ - vertex1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - vec1 = vertex1 - cells.at(iCell); + //Check for, and fix up periodicity + if(!spherical){ + vertex1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + vec1 = vertex1 - cells.at(iCell); - mag1 = vec1.magnitude(); - min_angle = 2.0*M_PI; - angle = 0.0; - swp_idx = -1; + mag1 = vec1.magnitude(); + min_angle = 2.0*M_PI; + angle = 0.0; + swp_idx = -1; - // Don't sort any vertices that have already been sorted. - for(k = j+1; k < verticesOnCell.at(iCell).size(); k++){ + // Don't sort any vertices that have already been sorted. + for(k = j+1; k < verticesOnCell.at(iCell).size(); k++){ #ifdef _DEBUG - cout << " Comparing " << j << " " << k << endl; + cout << " Comparing " << j << " " << k << endl; #endif - iVertex2 = verticesOnCell.at(iCell).at(k); + iVertex2 = verticesOnCell.at(iCell).at(k); - vertex2 = vertices.at(iVertex2); + vertex2 = vertices.at(iVertex2); - //Check for, and fix up periodicity - if(!spherical){ - vertex2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - vec2 = vertex2 - cells.at(iCell); + //Check for, and fix up periodicity + if(!spherical){ + vertex2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + vec2 = vertex2 - cells.at(iCell); - mag2 = vec2.magnitude(); + mag2 = vec2.magnitude(); - cross = vec1.cross(vec2); - dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); #ifdef _DEBUG - cout << " dot = " << dot << endl; + cout << " dot = " << dot << endl; #endif - // Only look at vertices that are CCW from the current vertex. - if(dot > 0){ - angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + // Only look at vertices that are CCW from the current vertex. + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); #ifdef _DEBUG - cout << " angle = " << angle << endl; + cout << " angle = " << angle << endl; #endif - if(angle < min_angle){ - min_angle = angle; - swp_idx = k; - } - } - } + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } - if(swp_idx != -1 && swp_idx != j+1){ - swp = verticesOnCell.at(iCell).at(j+1); - verticesOnCell.at(iCell).at(j+1) = verticesOnCell.at(iCell).at(swp_idx); - verticesOnCell.at(iCell).at(swp_idx) = swp; - } + if(swp_idx != -1 && swp_idx != j+1){ + swp = verticesOnCell.at(iCell).at(j+1); + verticesOnCell.at(iCell).at(j+1) = verticesOnCell.at(iCell).at(swp_idx); + verticesOnCell.at(iCell).at(swp_idx) = swp; + } #ifdef _DEBUG - if(swp_idx != -1 && swp_idx != j+1){ - cout << " swapped " << j << " " << swp_idx << endl; - } else { - cout << " no swap" << endl; - } - cout << " cellsOnVertex(" << iVertex1 << "): "; - for(k = 0; k < cellsOnVertex.at(iVertex1).size(); k++){ - cout << cellsOnVertex.at(iVertex1).at(k) << " "; - } - cout << endl; - iVertex2 = verticesOnCell.at(iCell).at(j+1); - cout << " cellsOnVertex(" << iVertex2 << "): "; - for(k = 0; k < cellsOnVertex.at(iVertex2).size(); k++){ - cout << cellsOnVertex.at(iVertex2).at(k) << " "; - } - cout << endl; -#endif - } - -#ifdef _DEBUG - cout << " Sorted verticesOnCell: "; - for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ - cout << verticesOnCell.at(iCell).at(j) << " "; - } - cout << endl; - for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ - cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; - for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ - cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; - } - cout << endl; - } -#endif - } + if(swp_idx != -1 && swp_idx != j+1){ + cout << " swapped " << j << " " << swp_idx << endl; + } else { + cout << " no swap" << endl; + } + cout << " cellsOnVertex(" << iVertex1 << "): "; + for(k = 0; k < cellsOnVertex.at(iVertex1).size(); k++){ + cout << cellsOnVertex.at(iVertex1).at(k) << " "; + } + cout << endl; + iVertex2 = verticesOnCell.at(iCell).at(j+1); + cout << " cellsOnVertex(" << iVertex2 << "): "; + for(k = 0; k < cellsOnVertex.at(iVertex2).size(); k++){ + cout << cellsOnVertex.at(iVertex2).at(k) << " "; + } + cout << endl; +#endif + } + +#ifdef _DEBUG + cout << " Sorted verticesOnCell: "; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << verticesOnCell.at(iCell).at(j) << " "; + } + cout << endl; + for(j = 0; j < verticesOnCell.at(iCell).size(); j++){ + cout << " cellsOnVertex " << verticesOnCell.at(iCell).at(j) << ": "; + for(k = 0; k < cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).size(); k++){ + cout << cellsOnVertex.at(verticesOnCell.at(iCell).at(j)).at(k) << " "; + } + cout << endl; + } +#endif + } - return 0; + return 0; }/*}}}*/ int buildCompleteCellMask(){/*{{{*/ - /* - * The buildCompleteCellMask function parses an ordered - * verticesOnCell field to determine if a cell is complete. It takes each - * vertex-vertex pair (from verticesOnCell) and determines the angle - * between them. If only one vertex is shared, the "edge" is skipped. - * - * These angles are summed up around each cell. If the total - * angle is close to 2.0*Pi then the cell is marked as complete. Otherwise - * it is marked as incomplete. - * - * The complete check is used when building edges. If an edge only has one - * vertex, but is connected to two complete cells, the edge is not added to - * the set. This resolves an issue related to quad grids, where a possible - * edge connects two cells across a vertex (where the edge mid point would - * be the vertex location). Using this check removes these edges from the - * possible set of edges. - * - */ - int iCell, iCell2, vertex1, vertex2; - int j, k, l; - pnt vert_loc1, vert_loc2; - pnt vec1, vec2; - pnt cross, normal; - double angle, angle_sum, dot; - bool complete; - - completeCellMask.clear(); - completeCellMask.resize(cells.size()); - - if(!spherical) { - normal = pnt(0.0, 0.0, 1.0); - } + /* + * The buildCompleteCellMask function parses an ordered + * verticesOnCell field to determine if a cell is complete. It takes each + * vertex-vertex pair (from verticesOnCell) and determines the angle + * between them. If only one vertex is shared, the "edge" is skipped. + * + * These angles are summed up around each cell. If the total + * angle is close to 2.0*Pi then the cell is marked as complete. Otherwise + * it is marked as incomplete. + * + * The complete check is used when building edges. If an edge only has one + * vertex, but is connected to two complete cells, the edge is not added to + * the set. This resolves an issue related to quad grids, where a possible + * edge connects two cells across a vertex (where the edge mid point would + * be the vertex location). Using this check removes these edges from the + * possible set of edges. + * + */ + int iCell, iCell2, vertex1, vertex2; + int j, k, l; + pnt vert_loc1, vert_loc2; + pnt vec1, vec2; + pnt cross, normal; + double angle, angle_sum, dot; + bool complete; + + completeCellMask.clear(); + completeCellMask.resize(cells.size()); + + if(!spherical) { + normal = pnt(0.0, 0.0, 1.0); + } - // Iterate over all cells - for(iCell = 0; iCell < cells.size(); iCell++){ - complete = false; - angle_sum = 0.0; + // Iterate over all cells + for(iCell = 0; iCell < cells.size(); iCell++){ + complete = false; + angle_sum = 0.0; - if(spherical){ - normal = cells.at(iCell); - } + if(spherical){ + normal = cells.at(iCell); + } #ifdef _DEBUG - cout << " Checking " << iCell << " for completeness" << endl; - cout << " " << cells.at(iCell) << endl; + cout << " Checking " << iCell << " for completeness" << endl; + cout << " " << cells.at(iCell) << endl; #endif - // Since verticesOnEdge is ordered already, compute angles between - // neighboring vertex/vertex pairs if they are both valid vertices. Sum - // the angles, and if the angles are close to 2.0 * Pi then the cell is - // "complete" - if(verticesOnCell.at(iCell).size() >= cellsOnCell.at(iCell).size()){ - for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ - vertex1 = verticesOnCell.at(iCell).at(j); - vertex2 = verticesOnCell.at(iCell).at(j+1); + // Since verticesOnEdge is ordered already, compute angles between + // neighboring vertex/vertex pairs if they are both valid vertices. Sum + // the angles, and if the angles are close to 2.0 * Pi then the cell is + // "complete" + if(verticesOnCell.at(iCell).size() >= cellsOnCell.at(iCell).size()){ + for(j = 0; j < verticesOnCell.at(iCell).size()-1; j++){ + vertex1 = verticesOnCell.at(iCell).at(j); + vertex2 = verticesOnCell.at(iCell).at(j+1); - if(vertex1 != -1 && vertex2 != -1){ - vert_loc1 = vertices.at(vertex1); - vert_loc2 = vertices.at(vertex2); + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); - if(!spherical){ - vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } - vec1 = vert_loc1 - cells.at(iCell); - vec2 = vert_loc2 - cells.at(iCell); + vec1 = vert_loc1 - cells.at(iCell); + vec2 = vert_loc2 - cells.at(iCell); - cross = vec1.cross(vec2); - dot = cross.dot(normal); + cross = vec1.cross(vec2); + dot = cross.dot(normal); #ifdef _DEBUG - cout << " vec1 : " << vec1 << endl; - cout << " vec2 : " << vec2 << endl; - cout << " cross : " << cross << endl; - cout << " dot : " << dot << endl; + cout << " vec1 : " << vec1 << endl; + cout << " vec2 : " << vec2 << endl; + cout << " cross : " << cross << endl; + cout << " dot : " << dot << endl; #endif - if(dot > 0){ + if(dot > 0){ - angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); - angle_sum += angle; + angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); + angle_sum += angle; #ifdef _DEBUG - cout << " adding angle (rad) : " << angle << endl; - cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; - cout << " new sum : " << angle_sum << endl; + cout << " adding angle (rad) : " << angle << endl; + cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; + cout << " new sum : " << angle_sum << endl; #endif - } else { - cout << " complete check has non CCW edge..." << endl; - } - } - } + } else { + cout << " complete check has non CCW edge..." << endl; + } + } + } - vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); - vertex2 = verticesOnCell.at(iCell).at(0); + vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); + vertex2 = verticesOnCell.at(iCell).at(0); - if(vertex1 != -1 && vertex2 != -1){ - vert_loc1 = vertices.at(vertex1); - vert_loc2 = vertices.at(vertex2); + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); - if(!spherical){ - vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } - vec1 = vert_loc1 - cells.at(iCell); - vec2 = vert_loc2 - cells.at(iCell); + vec1 = vert_loc1 - cells.at(iCell); + vec2 = vert_loc2 - cells.at(iCell); - cross = vec1.cross(vec2); - dot = cross.dot(normal); + cross = vec1.cross(vec2); + dot = cross.dot(normal); #ifdef _DEBUG - cout << " vec1 : " << vec1 << endl; - cout << " vec2 : " << vec2 << endl; - cout << " cross : " << cross << endl; - cout << " dot : " << dot << endl; + cout << " vec1 : " << vec1 << endl; + cout << " vec2 : " << vec2 << endl; + cout << " cross : " << cross << endl; + cout << " dot : " << dot << endl; #endif - if(dot > 0) { + if(dot > 0) { - angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); - angle_sum += angle; + angle = acos( vec2.dot(vec1) / (vec1.magnitude() * vec2.magnitude())); + angle_sum += angle; #ifdef _DEBUG - cout << " adding angle (rad) : " << angle << endl; - cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; - cout << " new sum : " << angle_sum << endl; + cout << " adding angle (rad) : " << angle << endl; + cout << " adding angle (deg) : " << angle * 180.0 / M_PI << endl; + cout << " new sum : " << angle_sum << endl; #endif - } else { - cout << " complete check has non CCW edge..." << endl; - } - } - } + } else { + cout << " complete check has non CCW edge..." << endl; + } + } + } - if(angle_sum > 2.0 * M_PI * 0.98){ - complete = true; - } + if(angle_sum > 2.0 * M_PI * 0.98){ + complete = true; + } #ifdef _DEBUG - cout << " Vertices on cell: " << verticesOnCell.at(iCell).size(); - cout << " Cells on cell: " << cellsOnCell.at(iCell).size(); - if(complete){ - cout << " Is complete!" << endl; - } else { - cout << " Is not complete!" << endl; - } + cout << " Vertices on cell: " << verticesOnCell.at(iCell).size(); + cout << " Cells on cell: " << cellsOnCell.at(iCell).size(); + if(complete){ + cout << " Is complete!" << endl; + } else { + cout << " Is not complete!" << endl; + } #endif - if(complete){ - completeCellMask.at(iCell) = 1; - } else { - completeCellMask.at(iCell) = 0; - } - } + if(complete){ + completeCellMask.at(iCell) = 1; + } else { + completeCellMask.at(iCell) = 0; + } + } - return 0; + return 0; }/*}}}*/ int buildEdges(){/*{{{*/ - /* - * buildEdges is intended to build a hash table that contains the - * cellsOnEdge and verticesonEdge pairs for each edge. The actual edge - * location is not constructed at this point in time, as we need to - * determine the four constituent locations for each edge before we can - * compute it. - * - * This function assumes the cellsOnVertex array is ordered such that - * two consecutive cell indices make up an edge. This isn't an issue - * for triangular dual grids, but quadrilateral dual grids have an issue - * where every cell is not connected with every other cell. So, the tool - * that generates input data for this program needs to ensure that - * ordering. - * - * After the hash table is created, it is iterated over to create each - * individual edge and ensure ordering is properly right handed. - * - */ - unordered_set edge_idx_hash; - unordered_set::iterator edge_idx_itr; - unordered_set::const_iterator edge_idx_search; - - int iCell, iVertex, iEdge, j, k, l; - int cell1, cell2; - int vertex1, vertex2, swp; - int land; - pair::iterator,bool> out_pair; - edge new_edge; - pnt edge_loc, normal; - pnt cell_loc1, cell_loc2, vert_loc1, vert_loc2, dist_vec; - pnt u_vec, v_vec, cross; - double vert1_x_movement, vert1_y_movement; - double vert2_x_movement, vert2_y_movement; - double temp, dot; - bool fixed_edge, add_edge; - -#ifdef _DEBUG - cout << endl << endl << "Begin function: buildEdges" << endl << endl; -#endif - - edge_idx_hash.clear(); - - land = 0; - - // Build all edges - for(iCell = 0; iCell < cells.size(); iCell++){ - if(completeCellMask.at(iCell) == 1) { - // Build edges from every vertex/vertex pair around a cell if the cell is complete - for(l = 0; l < verticesOnCell.at(iCell).size()-1; l++){ - vertex1 = verticesOnCell.at(iCell).at(l); - vertex2 = verticesOnCell.at(iCell).at(l+1); - - // Find cell shaerd by vertices, that's not iCell - cell1 = iCell; - cell2 = -1; - for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ - if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ - for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ - if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ - cell2 = cellsOnVertex.at(vertex1).at(j); - } - } - } - } - - add_edge = true; - -#ifdef _DEBUG - cout << "1 Starting edge: " << endl; - cout << " vertex 1 : " << vertex1 << endl; - cout << " vertex 2 : " << vertex2 << endl; - cout << " cell 1 : " << cell1 << endl; - cout << " cell 2 : " << cell2 << endl; -#endif - - if(vertex1 != -1 && vertex2 != -1){ - new_edge.vertex1 = min(vertex1, vertex2); - new_edge.vertex2 = max(vertex1, vertex2); - - if(cell2 != -1){ - new_edge.cell1 = min(cell1, cell2); - new_edge.cell2 = max(cell1, cell2); - } else { - new_edge.cell1 = cell1; - new_edge.cell2 = cell2; - } - - } else { - add_edge = false; - } - - if(add_edge){ -#ifdef _DEBUG - cout << " Adding edge" << endl; -#endif - out_pair = edge_idx_hash.insert(new_edge); - -#ifdef _DEBUG - if(out_pair.second == false){ - cout << " Failed to add edge." << endl; - } -#endif - } else { -#ifdef _DEBUG - cout << " Not adding edge" << endl; -#endif - } - - } - - vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); - vertex2 = verticesOnCell.at(iCell).at(0); - - // Find cell shaerd by vertices, that's not iCell - cell1 = iCell; - cell2 = -1; - for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ - if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ - for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ - if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ - cell2 = cellsOnVertex.at(vertex1).at(j); - } - } - } - } - - add_edge = true; - -#ifdef _DEBUG - cout << " Starting edge: " << endl; - cout << " vertex 1 : " << vertex1 << endl; - cout << " vertex 2 : " << vertex2 << endl; - cout << " cell 1 : " << cell1 << endl; - cout << " cell 2 : " << cell2 << endl; + /* + * buildEdges is intended to build a hash table that contains the + * cellsOnEdge and verticesonEdge pairs for each edge. The actual edge + * location is not constructed at this point in time, as we need to + * determine the four constituent locations for each edge before we can + * compute it. + * + * This function assumes the cellsOnVertex array is ordered such that + * two consecutive cell indices make up an edge. This isn't an issue + * for triangular dual grids, but quadrilateral dual grids have an issue + * where every cell is not connected with every other cell. So, the tool + * that generates input data for this program needs to ensure that + * ordering. + * + * After the hash table is created, it is iterated over to create each + * individual edge and ensure ordering is properly right handed. + * + */ + unordered_set edge_idx_hash; + unordered_set::iterator edge_idx_itr; + unordered_set::const_iterator edge_idx_search; + + int iCell, iVertex, iEdge, j, k, l; + int cell1, cell2; + int vertex1, vertex2, swp; + int land; + pair::iterator,bool> out_pair; + edge new_edge; + pnt edge_loc, normal; + pnt cell_loc1, cell_loc2, vert_loc1, vert_loc2, dist_vec; + pnt u_vec, v_vec, cross; + double vert1_x_movement, vert1_y_movement; + double vert2_x_movement, vert2_y_movement; + double temp, dot; + bool fixed_edge, add_edge; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildEdges" << endl << endl; +#endif + + edge_idx_hash.clear(); + + land = 0; + + // Build all edges + for(iCell = 0; iCell < cells.size(); iCell++){ + if(completeCellMask.at(iCell) == 1) { + // Build edges from every vertex/vertex pair around a cell if the cell is complete + for(l = 0; l < verticesOnCell.at(iCell).size()-1; l++){ + vertex1 = verticesOnCell.at(iCell).at(l); + vertex2 = verticesOnCell.at(iCell).at(l+1); + + // Find cell shaerd by vertices, that's not iCell + cell1 = iCell; + cell2 = -1; + for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ + if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ + for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ + if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ + cell2 = cellsOnVertex.at(vertex1).at(j); + } + } + } + } + + add_edge = true; + +#ifdef _DEBUG + cout << "1 Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; +#endif + + if(vertex1 != -1 && vertex2 != -1){ + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); + + if(cell2 != -1){ + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } else { + new_edge.cell1 = cell1; + new_edge.cell2 = cell2; + } + + } else { + add_edge = false; + } + + if(add_edge){ +#ifdef _DEBUG + cout << " Adding edge" << endl; +#endif + out_pair = edge_idx_hash.insert(new_edge); + +#ifdef _DEBUG + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } +#endif + } else { +#ifdef _DEBUG + cout << " Not adding edge" << endl; +#endif + } + + } + + vertex1 = verticesOnCell.at(iCell).at(verticesOnCell.at(iCell).size() - 1); + vertex2 = verticesOnCell.at(iCell).at(0); + + // Find cell shaerd by vertices, that's not iCell + cell1 = iCell; + cell2 = -1; + for(j = 0; j < cellsOnVertex.at(vertex1).size(); j++){ + if(cellsOnVertex.at(vertex1).at(j) != -1 && cellsOnVertex.at(vertex1).at(j) != iCell){ + for(k = 0; k < cellsOnVertex.at(vertex2).size(); k++){ + if(cellsOnVertex.at(vertex1).at(j) == cellsOnVertex.at(vertex2).at(k)){ + cell2 = cellsOnVertex.at(vertex1).at(j); + } + } + } + } + + add_edge = true; + +#ifdef _DEBUG + cout << " Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; #endif - if(vertex1 != -1 && vertex2 != -1){ - new_edge.vertex1 = min(vertex1, vertex2); - new_edge.vertex2 = max(vertex1, vertex2); + if(vertex1 != -1 && vertex2 != -1){ + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); - if(cell2 != -1){ - new_edge.cell1 = min(cell1, cell2); - new_edge.cell2 = max(cell1, cell2); - } else { - new_edge.cell1 = cell1; - new_edge.cell2 = cell2; - } + if(cell2 != -1){ + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } else { + new_edge.cell1 = cell1; + new_edge.cell2 = cell2; + } - } else { - add_edge = false; - } + } else { + add_edge = false; + } - if(add_edge){ + if(add_edge){ #ifdef _DEBUG - cout << " Adding edge" << endl; + cout << " Adding edge" << endl; #endif - out_pair = edge_idx_hash.insert(new_edge); + out_pair = edge_idx_hash.insert(new_edge); #ifdef _DEBUG - if(out_pair.second == false){ - cout << " Failed to add edge." << endl; - } + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } #endif - } else { + } else { #ifdef _DEBUG - cout << " Not adding edge" << endl; + cout << " Not adding edge" << endl; #endif - } + } - } else { - // Build edges from every cell/cell pair only if cell is not complete - for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ - cell1 = iCell; - cell2 = cellsOnCell.at(iCell).at(l); + } else { + // Build edges from every cell/cell pair only if cell is not complete + for(l = 0; l < cellsOnCell.at(iCell).size(); l++){ + cell1 = iCell; + cell2 = cellsOnCell.at(iCell).at(l); - // Find vertex pair for cell pair - vertex1 = -1; - vertex2 = -1; + // Find vertex pair for cell pair + vertex1 = -1; + vertex2 = -1; - for(j = 0; j < verticesOnCell.at(cell1).size(); j++){ - if(cell2 != -1){ - for(k = 0; k < verticesOnCell.at(cell2).size(); k++){ - if(verticesOnCell.at(cell1).at(j) == verticesOnCell.at(cell2).at(k)) { - if(vertex1 == -1){ - vertex1 = verticesOnCell.at(cell1).at(j); - } else if(vertex2 == -1) { - vertex2 = verticesOnCell.at(cell1).at(j); - } else { - cout << " Found more than 2 vertices for edge? " << endl; - } - } - } - } - } + for(j = 0; j < verticesOnCell.at(cell1).size(); j++){ + if(cell2 != -1){ + for(k = 0; k < verticesOnCell.at(cell2).size(); k++){ + if(verticesOnCell.at(cell1).at(j) == verticesOnCell.at(cell2).at(k)) { + if(vertex1 == -1){ + vertex1 = verticesOnCell.at(cell1).at(j); + } else if(vertex2 == -1) { + vertex2 = verticesOnCell.at(cell1).at(j); + } else { + cout << " Found more than 2 vertices for edge? " << endl; + } + } + } + } + } - if(vertex2 == -1) { - new_edge.vertex1 = vertex1; - new_edge.vertex2 = vertex2; - } else { - new_edge.vertex1 = min(vertex1, vertex2); - new_edge.vertex2 = max(vertex1, vertex2); - } + if(vertex2 == -1) { + new_edge.vertex1 = vertex1; + new_edge.vertex2 = vertex2; + } else { + new_edge.vertex1 = min(vertex1, vertex2); + new_edge.vertex2 = max(vertex1, vertex2); + } -#ifdef _DEBUG - cout << " Starting edge: " << endl; - cout << " vertex 1 : " << vertex1 << endl; - cout << " vertex 2 : " << vertex2 << endl; - cout << " cell 1 : " << cell1 << endl; - cout << " cell 2 : " << cell2 << endl; - - if(new_edge.vertex1 == -1){ - cout << " Edge is missing vertex 1" << endl; - } else if(new_edge.vertex2 == -1){ - cout << " Edge is missing vertex 2" << endl; - } -#endif - - if(cell2 == -1){ - new_edge.cell1 = cell1; - new_edge.cell2 = -1; - } else { - new_edge.cell1 = min(cell1, cell2); - new_edge.cell2 = max(cell1, cell2); - } - add_edge = true; +#ifdef _DEBUG + cout << " Starting edge: " << endl; + cout << " vertex 1 : " << vertex1 << endl; + cout << " vertex 2 : " << vertex2 << endl; + cout << " cell 1 : " << cell1 << endl; + cout << " cell 2 : " << cell2 << endl; + + if(new_edge.vertex1 == -1){ + cout << " Edge is missing vertex 1" << endl; + } else if(new_edge.vertex2 == -1){ + cout << " Edge is missing vertex 2" << endl; + } +#endif + + if(cell2 == -1){ + new_edge.cell1 = cell1; + new_edge.cell2 = -1; + } else { + new_edge.cell1 = min(cell1, cell2); + new_edge.cell2 = max(cell1, cell2); + } + add_edge = true; - if(new_edge.vertex1 == -1 || new_edge.vertex2 == -1){ - if(new_edge.cell1 == -1 || new_edge.cell2 == -1){ - add_edge = false; - } - -#ifdef _DEBUG - cout << " Cell 1 complete: " << completeCellMask.at(new_edge.cell1) << endl; - if(new_edge.cell2 != -1) { - cout << " Cell 2 complete: " << completeCellMask.at(new_edge.cell2) << endl; - } -#endif - - if(completeCellMask.at(new_edge.cell1) != 0 || + if(new_edge.vertex1 == -1 || new_edge.vertex2 == -1){ + if(new_edge.cell1 == -1 || new_edge.cell2 == -1){ + add_edge = false; + } + +#ifdef _DEBUG + cout << " Cell 1 complete: " << completeCellMask.at(new_edge.cell1) << endl; + if(new_edge.cell2 != -1) { + cout << " Cell 2 complete: " << completeCellMask.at(new_edge.cell2) << endl; + } +#endif + + if(completeCellMask.at(new_edge.cell1) != 0 || (new_edge.cell2 != -1 && completeCellMask.at(new_edge.cell2) != 0) ){ - add_edge = false; - } - } - - - if(add_edge){ -#ifdef _DEBUG - cout << " Adding edge" << endl; -#endif - out_pair = edge_idx_hash.insert(new_edge); -#ifdef _DEBUG - if(out_pair.second == false){ - cout << " Failed to add edge." << endl; - } -#endif - } else { -#ifdef _DEBUG - cout << " Not adding edge" << endl; -#endif - } - } - } - } - - cout << "Built " << edge_idx_hash.size() << " edge indices..." << endl; - - cellsOnEdge.clear(); - verticesOnEdge.clear(); - dvEdge.clear(); - dcEdge.clear(); - cellsOnEdge.resize(edge_idx_hash.size()); - verticesOnEdge.resize(edge_idx_hash.size()); - dvEdge.resize(edge_idx_hash.size()); - dcEdge.resize(edge_idx_hash.size()); - - if(!spherical){ - normal = pnt(0.0, 0.0, 1.0); - } - - iEdge = 0; - for(edge_idx_itr = edge_idx_hash.begin(); edge_idx_itr != edge_idx_hash.end(); ++edge_idx_itr){ -#ifdef _DEBUG - cout << "new edge: " << endl; -#endif - fixed_edge = false; - cell1 = (*edge_idx_itr).cell1; - cell2 = (*edge_idx_itr).cell2; - vertex1 = (*edge_idx_itr).vertex1; - vertex2 = (*edge_idx_itr).vertex2; - - cell_loc1 = cells.at(cell1); - vert_loc1 = vertices.at(vertex1); - if(vertex2 != -1) { - vert_loc2 = vertices.at(vertex2); - } else { - vert_loc2 = vert_loc1; - } - - if(spherical){ - normal = cells.at(cell1); - } else { - // Clean up periodic edges. See mesh specification document for how periodicity is defined. - // These edges are special in the sense that they must be across a "uniform" edge. - // So, they are exactly the mid point between vertices. - // Since the edge will be "owned" by whatever processor owns cell1, make sure edge is close to cell1. - // So, fix periodicity relative to cell1. - vert_loc1.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); - } - - if(cell2 != -1){ - cell_loc2 = cells.at(cell2); - if(vertex2 != -1){ - vert_loc2 = vertices.at(vertex2); - - if(!spherical) { - cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); - vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); - edge_loc = planarIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); - dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); - dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); - } else { // If spherical - edge_loc = gcIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); - dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); - dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); - } - } else { - edge_loc = (cell_loc1 + cell_loc2) * 0.5; - vert_loc2 = edge_loc; - if(!spherical){ - dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); - dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); - } else { - dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); - dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); - } - } - } else { - if(vertex2 != -1){ - vert_loc2 = vertices.at(vertex2); - if(!spherical){ - // Set dv and dc Edge values on plane - vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); - edge_loc = (vert_loc1 + vert_loc2) * 0.5; - dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); - dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); - } else { - // Set dv and dc Edge values on Sphere - edge_loc = (vert_loc1 + vert_loc2) * 0.5; - edge_loc.normalize(); - dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); - dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); - } - cell_loc2 = edge_loc; - } else { - cout << " ERROR: Edge found with only 1 cell and 1 vertex...." << endl; - return 1; - } - } - - if(spherical) edge_loc.normalize(); - - edge_loc.idx = iEdge; - -#ifdef _DEBUG - cout << "New Edge At: " << edge_loc << endl; - cout << " c1: " << cells.at(cell1) << endl; - if(cell2 > -1) { - cout << " c2: " << cells.at(cell2) << endl; - cout << " mod? c2: " << cell_loc2 << endl; - } else { - cout << " c2: land" << endl; - cout << " mod? c2: " << cell_loc2 << endl; - } - cout << " v1: " << vertices.at(vertex1) << endl; - cout << " mod? v1: " << vert_loc1 << endl; - if(vertex2 != -1) { - cout << " v2: " << vertices.at(vertex2) << endl; - cout << " mod? v2: " << vert_loc2 << endl; - } else { - cout << " v2: land" << endl; - cout << " mod? v2: " << vert_loc2 << endl; - } -#endif - - u_vec = cell_loc2 - cell_loc1; - v_vec = vert_loc2 - vert_loc1; - - cross = u_vec.cross(v_vec); - dot = cross.dot(normal); - - if(dot < 0){ - if(vertex2 != -1){ -#ifdef _DEBUG - cout << " swapping vertex " << vertex1 << " and " << vertex2 << endl; -#endif - swp = vertex2; - vertex2 = vertex1; - vertex1 = swp; - } else { -#ifdef _DEBUG - cout << " swapping cell " << cell1 << " and " << cell2 << endl; -#endif - swp = cell2; - cell2 = cell1; - cell1 = swp; - } - } - - edges.push_back(edge_loc); - cellsOnEdge.at(iEdge).clear(); - cellsOnEdge.at(iEdge).push_back(cell1); - cellsOnEdge.at(iEdge).push_back(cell2); - verticesOnEdge.at(iEdge).clear(); - verticesOnEdge.at(iEdge).push_back(vertex1); - verticesOnEdge.at(iEdge).push_back(vertex2); - - iEdge++; - } - - edge_idx_hash.clear(); - - return 0; + add_edge = false; + } + } + + + if(add_edge){ +#ifdef _DEBUG + cout << " Adding edge" << endl; +#endif + out_pair = edge_idx_hash.insert(new_edge); +#ifdef _DEBUG + if(out_pair.second == false){ + cout << " Failed to add edge." << endl; + } +#endif + } else { +#ifdef _DEBUG + cout << " Not adding edge" << endl; +#endif + } + } + } + } + + cout << "Built " << edge_idx_hash.size() << " edge indices..." << endl; + + cellsOnEdge.clear(); + verticesOnEdge.clear(); + dvEdge.clear(); + dcEdge.clear(); + cellsOnEdge.resize(edge_idx_hash.size()); + verticesOnEdge.resize(edge_idx_hash.size()); + dvEdge.resize(edge_idx_hash.size()); + dcEdge.resize(edge_idx_hash.size()); + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } + + iEdge = 0; + for(edge_idx_itr = edge_idx_hash.begin(); edge_idx_itr != edge_idx_hash.end(); ++edge_idx_itr){ +#ifdef _DEBUG + cout << "new edge: " << endl; +#endif + fixed_edge = false; + cell1 = (*edge_idx_itr).cell1; + cell2 = (*edge_idx_itr).cell2; + vertex1 = (*edge_idx_itr).vertex1; + vertex2 = (*edge_idx_itr).vertex2; + + cell_loc1 = cells.at(cell1); + vert_loc1 = vertices.at(vertex1); + if(vertex2 != -1) { + vert_loc2 = vertices.at(vertex2); + } else { + vert_loc2 = vert_loc1; + } + + if(spherical){ + normal = cells.at(cell1); + } else { + // Clean up periodic edges. See mesh specification document for how periodicity is defined. + // These edges are special in the sense that they must be across a "uniform" edge. + // So, they are exactly the mid point between vertices. + // Since the edge will be "owned" by whatever processor owns cell1, make sure edge is close to cell1. + // So, fix periodicity relative to cell1. + vert_loc1.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + } + + if(cell2 != -1){ + cell_loc2 = cells.at(cell2); + if(vertex2 != -1){ + vert_loc2 = vertices.at(vertex2); + + if(!spherical) { + cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + edge_loc = planarIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); + dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); + dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); + } else { // If spherical + edge_loc = gcIntersect(cell_loc1, cell_loc2, vert_loc1, vert_loc2); + dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); + dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); + } + } else { + edge_loc = (cell_loc1 + cell_loc2) * 0.5; + vert_loc2 = edge_loc; + if(!spherical){ + dcEdge.at(iEdge) = (cell_loc2 - cell_loc1).magnitude(); + dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); + } else { + dcEdge.at(iEdge) = cell_loc2.sphereDistance(cell_loc1); + dvEdge.at(iEdge) = dcEdge.at(iEdge) / sqrt(3.0); + } + } + } else { + if(vertex2 != -1){ + vert_loc2 = vertices.at(vertex2); + if(!spherical){ + // Set dv and dc Edge values on plane + vert_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + edge_loc = (vert_loc1 + vert_loc2) * 0.5; + dvEdge.at(iEdge) = (vert_loc2 - vert_loc1).magnitude(); + dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); + } else { + // Set dv and dc Edge values on Sphere + edge_loc = (vert_loc1 + vert_loc2) * 0.5; + edge_loc.normalize(); + dvEdge.at(iEdge) = vert_loc2.sphereDistance(vert_loc1); + dcEdge.at(iEdge) = sqrt(3.0) * dvEdge.at(iEdge); + } + cell_loc2 = edge_loc; + } else { + cout << " ERROR: Edge found with only 1 cell and 1 vertex...." << endl; + return 1; + } + } + + if(spherical) edge_loc.normalize(); + + edge_loc.idx = iEdge; + +#ifdef _DEBUG + cout << "New Edge At: " << edge_loc << endl; + cout << " c1: " << cells.at(cell1) << endl; + if(cell2 > -1) { + cout << " c2: " << cells.at(cell2) << endl; + cout << " mod? c2: " << cell_loc2 << endl; + } else { + cout << " c2: land" << endl; + cout << " mod? c2: " << cell_loc2 << endl; + } + cout << " v1: " << vertices.at(vertex1) << endl; + cout << " mod? v1: " << vert_loc1 << endl; + if(vertex2 != -1) { + cout << " v2: " << vertices.at(vertex2) << endl; + cout << " mod? v2: " << vert_loc2 << endl; + } else { + cout << " v2: land" << endl; + cout << " mod? v2: " << vert_loc2 << endl; + } +#endif + + u_vec = cell_loc2 - cell_loc1; + v_vec = vert_loc2 - vert_loc1; + + cross = u_vec.cross(v_vec); + dot = cross.dot(normal); + + if(dot < 0){ + if(vertex2 != -1){ +#ifdef _DEBUG + cout << " swapping vertex " << vertex1 << " and " << vertex2 << endl; +#endif + swp = vertex2; + vertex2 = vertex1; + vertex1 = swp; + } else { +#ifdef _DEBUG + cout << " swapping cell " << cell1 << " and " << cell2 << endl; +#endif + swp = cell2; + cell2 = cell1; + cell1 = swp; + } + } + + edges.push_back(edge_loc); + cellsOnEdge.at(iEdge).clear(); + cellsOnEdge.at(iEdge).push_back(cell1); + cellsOnEdge.at(iEdge).push_back(cell2); + verticesOnEdge.at(iEdge).clear(); + verticesOnEdge.at(iEdge).push_back(vertex1); + verticesOnEdge.at(iEdge).push_back(vertex2); + + iEdge++; + } + + edge_idx_hash.clear(); + + return 0; }/*}}}*/ int orderVertexArrays(){/*{{{*/ - /* - * orderVertexArrays builds and orders the connectivity arrays for vertices. - * This includes edgesOnVertex and cellsOnvertex. - * First, edgeSOnVertex is built and ordered correctly, then using - * that ordering cellsOnVertex is built and ordered correctly as well. - */ - int iEdge, iVertex, vertex1, vertex2, cell1, cell2, edge1, edge2; - int i, j, k, l, swp_idx, swp; - pnt normal; - pnt cross; - pnt vec1, vec2; - pnt cell_loc, edge_loc1, edge_loc2; - double mag1, mag2, min_angle, angle; - double dvEdge, dot, area; - bool fixed_area; - -#ifdef _DEBUG - cout << endl << endl << "Begin function: orderVertexArrays" << endl << endl; -#endif - - edgesOnVertex.clear(); - edgesOnVertex.resize(vertices.size()); - cellsOnVertex.clear(); - cellsOnVertex.resize(vertices.size()); - - // Get lists of edges for each vertex - for(iEdge = 0; iEdge < edges.size(); iEdge++){/*{{{*/ - vertex1 = verticesOnEdge.at(iEdge).at(0); - vertex2 = verticesOnEdge.at(iEdge).at(1); - - if(vertex1 != -1) { - edgesOnVertex.at(vertex1).push_back(iEdge); - } - - if(vertex2 != -1){ - edgesOnVertex.at(vertex2).push_back(iEdge); - } - }/*}}}*/ - - if(!spherical){ - normal = pnt(0.0, 0.0, 1.0); - } + /* + * orderVertexArrays builds and orders the connectivity arrays for vertices. + * This includes edgesOnVertex and cellsOnvertex. + * First, edgeSOnVertex is built and ordered correctly, then using + * that ordering cellsOnVertex is built and ordered correctly as well. + */ + int iEdge, iVertex, vertex1, vertex2, cell1, cell2, edge1, edge2; + int i, j, k, l, swp_idx, swp; + pnt normal; + pnt cross; + pnt vec1, vec2; + pnt cell_loc, edge_loc1, edge_loc2; + double mag1, mag2, min_angle, angle; + double dvEdge, dot, area; + bool fixed_area; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: orderVertexArrays" << endl << endl; +#endif + + edgesOnVertex.clear(); + edgesOnVertex.resize(vertices.size()); + cellsOnVertex.clear(); + cellsOnVertex.resize(vertices.size()); + + // Get lists of edges for each vertex + for(iEdge = 0; iEdge < edges.size(); iEdge++){/*{{{*/ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + + if(vertex1 != -1) { + edgesOnVertex.at(vertex1).push_back(iEdge); + } + + if(vertex2 != -1){ + edgesOnVertex.at(vertex2).push_back(iEdge); + } + }/*}}}*/ + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } - // Order edges counter-clockwise - for(iVertex = 0; iVertex < vertices.size(); iVertex++){/*{{{*/ - if(spherical){ - normal = vertices.at(iVertex); - } + // Order edges counter-clockwise + for(iVertex = 0; iVertex < vertices.size(); iVertex++){/*{{{*/ + if(spherical){ + normal = vertices.at(iVertex); + } - //Loop over all edges except the last two as a starting edge. - for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ - edge1 = edgesOnVertex.at(iVertex).at(j); + //Loop over all edges except the last two as a starting edge. + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + edge1 = edgesOnVertex.at(iVertex).at(j); - edge_loc1 = edges.at(edge1); + edge_loc1 = edges.at(edge1); - // Fix edge1 periodicity - if(!spherical){ - edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); - } - vec1 = edge_loc1 - vertices.at(iVertex); + // Fix edge1 periodicity + if(!spherical){ + edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + } + vec1 = edge_loc1 - vertices.at(iVertex); - mag1 = vec1.magnitude(); - min_angle = 2.0*M_PI; - angle = 0.0; - swp_idx = -1; + mag1 = vec1.magnitude(); + min_angle = 2.0*M_PI; + angle = 0.0; + swp_idx = -1; - //Don't sort any edges that have already been sorted. - for(k = j+1; k < edgesOnVertex.at(iVertex).size(); k++){ - edge2 = edgesOnVertex.at(iVertex).at(k); + //Don't sort any edges that have already been sorted. + for(k = j+1; k < edgesOnVertex.at(iVertex).size(); k++){ + edge2 = edgesOnVertex.at(iVertex).at(k); - edge_loc2 = edges.at(edge2); + edge_loc2 = edges.at(edge2); - // Fix edge2 periodicity - if(!spherical){ - edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); - } - vec2 = edge_loc2 - vertices.at(iVertex); - mag2 = vec2.magnitude(); + // Fix edge2 periodicity + if(!spherical){ + edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + } + vec2 = edge_loc2 - vertices.at(iVertex); + mag2 = vec2.magnitude(); - cross = vec1.cross(vec2); - dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); - angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); - // Only look at vertices that are CCW from the current edge. - if(dot > 0){ - angle = acos(vec1.dot(vec2) / (mag1 * mag2)); - if(angle < min_angle){ - min_angle = angle; - swp_idx = k; - } - } - } + // Only look at vertices that are CCW from the current edge. + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } - if(swp_idx != -1 && swp_idx != j+1){ - swp = edgesOnVertex.at(iVertex).at(j+1); - edgesOnVertex.at(iVertex).at(j+1) = edgesOnVertex.at(iVertex).at(swp_idx); - edgesOnVertex.at(iVertex).at(swp_idx) = swp; - } - } + if(swp_idx != -1 && swp_idx != j+1){ + swp = edgesOnVertex.at(iVertex).at(j+1); + edgesOnVertex.at(iVertex).at(j+1) = edgesOnVertex.at(iVertex).at(swp_idx); + edgesOnVertex.at(iVertex).at(swp_idx) = swp; + } + } #ifdef _DEBUG - cout << "edgesOnVertex("<< iVertex <<"): "; - for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ - cout << edgesOnVertex.at(iVertex).at(j) << " "; - } - cout << endl; + cout << "edgesOnVertex("<< iVertex <<"): "; + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + cout << edgesOnVertex.at(iVertex).at(j) << " "; + } + cout << endl; #endif - cellsOnVertex.at(iVertex).clear(); + cellsOnVertex.at(iVertex).clear(); #ifdef _DEBUG - cout << "CellsOnVertex("<< iVertex << "): "; + cout << "CellsOnVertex("<< iVertex << "): "; #endif - // Using the ordered edges. Buld cellsOnVertex in the correct order. - for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ - edge1 = edgesOnVertex.at(iVertex).at(j); - fixed_area = false; + // Using the ordered edges. Buld cellsOnVertex in the correct order. + for(j = 0; j < edgesOnVertex.at(iVertex).size(); j++){ + edge1 = edgesOnVertex.at(iVertex).at(j); + fixed_area = false; - // Get cell id and add it to list of cells - if(iVertex == verticesOnEdge.at(edge1).at(0)){ - cell1 = cellsOnEdge.at(edge1).at(0); - cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(0) ); + // Get cell id and add it to list of cells + if(iVertex == verticesOnEdge.at(edge1).at(0)){ + cell1 = cellsOnEdge.at(edge1).at(0); + cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(0) ); #ifdef _DEBUG - cout << cellsOnEdge.at(edge1).at(0) << " "; + cout << cellsOnEdge.at(edge1).at(0) << " "; #endif - } else { - cell1 = cellsOnEdge.at(edge1).at(1); - cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(1) ); + } else { + cell1 = cellsOnEdge.at(edge1).at(1); + cellsOnVertex.at(iVertex).push_back( cellsOnEdge.at(edge1).at(1) ); #ifdef _DEBUG - cout << cellsOnEdge.at(edge1).at(1) << " "; + cout << cellsOnEdge.at(edge1).at(1) << " "; #endif - } - } + } + } #ifdef _DEBUG - cout << endl << endl; + cout << endl << endl; #endif - }/*}}}*/ + }/*}}}*/ - return 0; + return 0; }/*}}}*/ int orderCellArrays(){/*{{{*/ - /* - * orderCellArrays assumes verticesOnCell are ordered CCW already. - * - */ - int iCell, iVertex, iEdge, iEdge2, add_cell; - int cell1, cell2, vertex1, vertex2, prev_vertex; - int edge_idx, loc_edge_idx; - int i, j, k, swp_idx; - pnt normal, cross; - pnt vec1, vec2; - pnt vert_loc, edge_loc, cell_loc; - pnt edge_loc1, edge_loc2; - pnt next_edge_loc; - double swp; - double dot, mag1, mag2, angle, min_angle; - bool found, bad_vertices; - -#ifdef _DEBUG - cout << endl << endl << "Begin function: orderCellArrays" << endl << endl; -#endif - - maxEdges = 0; - - verticesOnCell.clear(); - edgesOnCell.clear(); - cellsOnCell.clear(); - - verticesOnCell.resize(cells.size()); - edgesOnCell.resize(cells.size()); - cellsOnCell.resize(cells.size()); - - if(!spherical){ - normal = pnt(0.0, 0.0, 1.0); - } + /* + * orderCellArrays assumes verticesOnCell are ordered CCW already. + * + */ + int iCell, iVertex, iEdge, iEdge2, add_cell; + int cell1, cell2, vertex1, vertex2, prev_vertex; + int edge_idx, loc_edge_idx; + int i, j, k, swp_idx; + pnt normal, cross; + pnt vec1, vec2; + pnt vert_loc, edge_loc, cell_loc; + pnt edge_loc1, edge_loc2; + pnt next_edge_loc; + double swp; + double dot, mag1, mag2, angle, min_angle; + bool found, bad_vertices; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: orderCellArrays" << endl << endl; +#endif + + maxEdges = 0; + + verticesOnCell.clear(); + edgesOnCell.clear(); + cellsOnCell.clear(); + + verticesOnCell.resize(cells.size()); + edgesOnCell.resize(cells.size()); + cellsOnCell.resize(cells.size()); + + if(!spherical){ + normal = pnt(0.0, 0.0, 1.0); + } - // First, build full list of edges on cell. - for(iEdge = 0; iEdge < edges.size(); iEdge++){ - cell1 = cellsOnEdge.at(iEdge).at(0); - cell2 = cellsOnEdge.at(iEdge).at(1); + // First, build full list of edges on cell. + for(iEdge = 0; iEdge < edges.size(); iEdge++){ + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); - edgesOnCell.at(cell1).push_back(iEdge); - if(cell2 != -1){ - edgesOnCell.at(cell2).push_back(iEdge); - } - } + edgesOnCell.at(cell1).push_back(iEdge); + if(cell2 != -1){ + edgesOnCell.at(cell2).push_back(iEdge); + } + } - // Loop over all cells. - for(iCell = 0; iCell < cells.size(); iCell++){ + // Loop over all cells. + for(iCell = 0; iCell < cells.size(); iCell++){ #ifdef _DEBUG - cout << "New Cell " << cells.at(iCell) << endl; + cout << "New Cell " << cells.at(iCell) << endl; #endif - if(spherical){ - normal = cells.at(iCell); - } + if(spherical){ + normal = cells.at(iCell); + } loc_edge_idx = 0; // if no edges on cell, apparently occurs for quad meshes - if ( edgesOnCell.at(iCell).size() != 0 ) { -#ifdef _DEBUG - cout << endl; - cout << " Starting edgesOnCell on cell: " << iCell << endl; - cout << " Starting edgesOnCell size: " << edgesOnCell.at(iCell).size() << endl; - cout << " Starting edgesOnCell: "; - for(i = 0; i < edgesOnCell.at(iCell).size(); ++i){ - cout << edgesOnCell.at(iCell).at(i) << " "; - } - cout << endl; -#endif - - // /* - // Determine starting edge. It should either be the first edge in the set, - // or the only edge such that all other edges are CCW from it. - edge_idx = edgesOnCell.at(iCell).at(0); - #ifdef _DEBUG - cout << "Finding starting edge for cell " << iCell << endl; - #endif - for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ - iEdge = edgesOnCell.at(iCell).at(j); - vertex1 = verticesOnEdge.at(iEdge).at(0); - vertex2 = verticesOnEdge.at(iEdge).at(1); - - if(vertex2 == -1){ - #ifdef _DEBUG - cout << " Start edge: " << iEdge << endl; - cout << " " << edges.at(iEdge) << endl; - cout << " v1: " << vertex1 << endl; - cout << " v2: " << vertex2 << endl; - #endif - edge_loc1 = edges.at(iEdge); - edge_loc1 = vertices.at(vertex1); - if(!spherical){ - edge_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - - vec1 = edge_loc1 - cells.at(iCell); - mag1 = vec1.magnitude(); - - // If edge only has one vertex. Need to find edge that shares the vertex. - // This edge is kept if the neighboring edge is CCW from it. - for(k = 0; k < edgesOnCell.at(iCell).size(); k++){ - if(j != k){ - iEdge2 = edgesOnCell.at(iCell).at(k); - #ifdef _DEBUG - cout << " Test edge: " << iEdge2 << endl; - cout << " " << edges.at(iEdge2) << endl; - cout << " v1: " << verticesOnEdge.at(iEdge2).at(0) << endl; - cout << " v2: " << verticesOnEdge.at(iEdge2).at(1) << endl; - #endif - - if(vertex1 == verticesOnEdge.at(iEdge2).at(0) || vertex1 == verticesOnEdge.at(iEdge2).at(1)){ - // This edge is a neighboring edge. Check for CCW ordering. - if(vertex1 == verticesOnEdge.at(iEdge2).at(0)) { - vertex2 = verticesOnEdge.at(iEdge2).at(1); - } else { - vertex2 = verticesOnEdge.at(iEdge2).at(0); - } - edge_loc2 = edges.at(iEdge2); - edge_loc2 = vertices.at(vertex2); - - if(!spherical){ - edge_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - - vec2 = edge_loc2 - cells.at(iCell); - mag2 = vec2.magnitude(); - - cross = vec1.cross(vec2); - dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); - - #ifdef _DEBUG - cout << " Vec1: " << vec1 << endl; - cout << " Vec2: " << vec2 << endl; - cout << " Cross: " << cross << endl; - cout << " dot: " << dot << endl; - #endif - if(dot > 0){ - #ifdef _DEBUG - cout << " Found edge: " << iEdge << endl; - #endif - edge_idx = iEdge; - loc_edge_idx = j; - } - } - } - } - } - } - } - - // Swap edge_idx with first edge. - if(loc_edge_idx != 0){ -#ifdef _DEBUG - cout << " Swapping edges: " << - edgesOnCell.at(iCell).at(loc_edge_idx) << + if ( edgesOnCell.at(iCell).size() != 0 ) { +#ifdef _DEBUG + cout << endl; + cout << " Starting edgesOnCell on cell: " << iCell << endl; + cout << " Starting edgesOnCell size: " << edgesOnCell.at(iCell).size() << endl; + cout << " Starting edgesOnCell: "; + for(i = 0; i < edgesOnCell.at(iCell).size(); ++i){ + cout << edgesOnCell.at(iCell).at(i) << " "; + } + cout << endl; +#endif + + // /* + // Determine starting edge. It should either be the first edge in the set, + // or the only edge such that all other edges are CCW from it. + edge_idx = edgesOnCell.at(iCell).at(0); + #ifdef _DEBUG + cout << "Finding starting edge for cell " << iCell << endl; + #endif + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + + if(vertex2 == -1){ + #ifdef _DEBUG + cout << " Start edge: " << iEdge << endl; + cout << " " << edges.at(iEdge) << endl; + cout << " v1: " << vertex1 << endl; + cout << " v2: " << vertex2 << endl; + #endif + edge_loc1 = edges.at(iEdge); + edge_loc1 = vertices.at(vertex1); + if(!spherical){ + edge_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = edge_loc1 - cells.at(iCell); + mag1 = vec1.magnitude(); + + // If edge only has one vertex. Need to find edge that shares the vertex. + // This edge is kept if the neighboring edge is CCW from it. + for(k = 0; k < edgesOnCell.at(iCell).size(); k++){ + if(j != k){ + iEdge2 = edgesOnCell.at(iCell).at(k); + #ifdef _DEBUG + cout << " Test edge: " << iEdge2 << endl; + cout << " " << edges.at(iEdge2) << endl; + cout << " v1: " << verticesOnEdge.at(iEdge2).at(0) << endl; + cout << " v2: " << verticesOnEdge.at(iEdge2).at(1) << endl; + #endif + + if(vertex1 == verticesOnEdge.at(iEdge2).at(0) || vertex1 == verticesOnEdge.at(iEdge2).at(1)){ + // This edge is a neighboring edge. Check for CCW ordering. + if(vertex1 == verticesOnEdge.at(iEdge2).at(0)) { + vertex2 = verticesOnEdge.at(iEdge2).at(1); + } else { + vertex2 = verticesOnEdge.at(iEdge2).at(0); + } + edge_loc2 = edges.at(iEdge2); + edge_loc2 = vertices.at(vertex2); + + if(!spherical){ + edge_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec2 = edge_loc2 - cells.at(iCell); + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + + #ifdef _DEBUG + cout << " Vec1: " << vec1 << endl; + cout << " Vec2: " << vec2 << endl; + cout << " Cross: " << cross << endl; + cout << " dot: " << dot << endl; + #endif + if(dot > 0){ + #ifdef _DEBUG + cout << " Found edge: " << iEdge << endl; + #endif + edge_idx = iEdge; + loc_edge_idx = j; + } + } + } + } + } + } + } + + // Swap edge_idx with first edge. + if(loc_edge_idx != 0){ +#ifdef _DEBUG + cout << " Swapping edges: " << + edgesOnCell.at(iCell).at(loc_edge_idx) << " and " << edgesOnCell.at(iCell).at(0) << endl; #endif - edgesOnCell.at(iCell).at(loc_edge_idx) = edgesOnCell.at(iCell).at(0); - edgesOnCell.at(iCell).at(0) = edge_idx; - } - // */ - - // Order all edges in CCW relative to the first edge. - // Loop over all vertices except the last two as a starting vertex. - for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ - iEdge = edgesOnCell.at(iCell).at(j); - edge_loc = edges.at(iEdge); - - // Add cell and vertex from first edge. - // Add cell across edge to cellsOnCell - // Also, add vertex that is CCW relative to current edge location. - if(cellsOnEdge.at(iEdge).at(0) == iCell){ - cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(1)); - - if(verticesOnEdge.at(iEdge).at(1) != -1){ - verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(1)); - } - } else { - cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(0)); - verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(0)); - } - - - if(!spherical){ - edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - - vec1 = edge_loc - cells.at(iCell); - mag1 = vec1.magnitude(); - - min_angle = 2.0 * M_PI; - angle = 0.0; - swp_idx = -1; - - for(k = j+1; k < edgesOnCell.at(iCell).size(); k++){ - iEdge2 = edgesOnCell.at(iCell).at(k); - - next_edge_loc = edges.at(iEdge2); - - if(!spherical){ - next_edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - } - - vec2 = next_edge_loc - cells.at(iCell); - mag2 = vec2.magnitude(); - - cross = vec1.cross(vec2); - dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); - - // Only look at edges that are CCW from the current edge - if(dot > 0){ - angle = acos(vec1.dot(vec2) / (mag1 * mag2)); - if(angle < min_angle){ - min_angle = angle; - swp_idx = k; - } - } - } - - if(swp_idx != -1 && swp_idx != j+1){ - swp = edgesOnCell.at(iCell).at(j+1); - edgesOnCell.at(iCell).at(j+1) = edgesOnCell.at(iCell).at(swp_idx); - edgesOnCell.at(iCell).at(swp_idx) = swp; - } - } - - -#ifdef _DEBUG - cout << " cellsOnCell: "; - for(i = 0; i < cellsOnCell.at(iCell).size(); i++){ - cout << cellsOnCell.at(iCell).at(i) << " "; - } - cout << endl; - cout << " verticesOnCell: "; - for(i = 0; i < verticesOnCell.at(iCell).size(); i++){ - cout << verticesOnCell.at(iCell).at(i) << " "; - } - cout << endl; - cout << " edgesOnCell: "; - for(i = 0; i < edgesOnCell.at(iCell).size(); i++){ - cout << edgesOnCell.at(iCell).at(i) << " "; - } - cout << endl; -#endif - - maxEdges = max(maxEdges, (int)edgesOnCell.at(iCell).size()); - } - - return 0; + edgesOnCell.at(iCell).at(loc_edge_idx) = edgesOnCell.at(iCell).at(0); + edgesOnCell.at(iCell).at(0) = edge_idx; + } + // */ + + // Order all edges in CCW relative to the first edge. + // Loop over all vertices except the last two as a starting vertex. + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + edge_loc = edges.at(iEdge); + + // Add cell and vertex from first edge. + // Add cell across edge to cellsOnCell + // Also, add vertex that is CCW relative to current edge location. + if(cellsOnEdge.at(iEdge).at(0) == iCell){ + cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(1)); + + if(verticesOnEdge.at(iEdge).at(1) != -1){ + verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(1)); + } + } else { + cellsOnCell.at(iCell).push_back(cellsOnEdge.at(iEdge).at(0)); + verticesOnCell.at(iCell).push_back(verticesOnEdge.at(iEdge).at(0)); + } + + + if(!spherical){ + edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec1 = edge_loc - cells.at(iCell); + mag1 = vec1.magnitude(); + + min_angle = 2.0 * M_PI; + angle = 0.0; + swp_idx = -1; + + for(k = j+1; k < edgesOnCell.at(iCell).size(); k++){ + iEdge2 = edgesOnCell.at(iCell).at(k); + + next_edge_loc = edges.at(iEdge2); + + if(!spherical){ + next_edge_loc.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + } + + vec2 = next_edge_loc - cells.at(iCell); + mag2 = vec2.magnitude(); + + cross = vec1.cross(vec2); + dot = cross.dot(normal) / (cross.magnitude() * normal.magnitude()); + + // Only look at edges that are CCW from the current edge + if(dot > 0){ + angle = acos(vec1.dot(vec2) / (mag1 * mag2)); + if(angle < min_angle){ + min_angle = angle; + swp_idx = k; + } + } + } + + if(swp_idx != -1 && swp_idx != j+1){ + swp = edgesOnCell.at(iCell).at(j+1); + edgesOnCell.at(iCell).at(j+1) = edgesOnCell.at(iCell).at(swp_idx); + edgesOnCell.at(iCell).at(swp_idx) = swp; + } + } + + +#ifdef _DEBUG + cout << " cellsOnCell: "; + for(i = 0; i < cellsOnCell.at(iCell).size(); i++){ + cout << cellsOnCell.at(iCell).at(i) << " "; + } + cout << endl; + cout << " verticesOnCell: "; + for(i = 0; i < verticesOnCell.at(iCell).size(); i++){ + cout << verticesOnCell.at(iCell).at(i) << " "; + } + cout << endl; + cout << " edgesOnCell: "; + for(i = 0; i < edgesOnCell.at(iCell).size(); i++){ + cout << edgesOnCell.at(iCell).at(i) << " "; + } + cout << endl; +#endif + + maxEdges = max(maxEdges, (int)edgesOnCell.at(iCell).size()); + } + + return 0; }/*}}}*/ int buildAreas(){/*{{{*/ - /* - * buildAreas constructs the area arrays. - * This includes areaCell, areaTriangle, and kiteAreasOnVertex - * - * Before constructing areaCell, the cell is checked for completeness. - * This is accomplished by looping over each edge. For each edge, - * the angle between it's vertices is computed and added to a running total. - * If the total is significantly less than 2*Pi, the cell is not complete. - * - * Non-complete cells are given a negative area, to ease removal at a later stage. - */ - int iVertex, iCell, iEdge, i, j; - int vertex1, vertex2; - int edge1, edge2; - int incomplete_cells; - pnt vert_loc1, vert_loc2; - pnt edge_loc1, edge_loc2; - pnt cell_loc; - pnt vec1, vec2; - -#ifdef _DEBUG - cout << endl << endl << "Begin function: buildAreas" << endl << endl; -#endif - - areaCell.clear(); - areaTriangle.clear(); - kiteAreasOnVertex.clear(); - - areaCell.resize(cells.size()); - areaTriangle.resize(vertices.size()); - kiteAreasOnVertex.resize(vertices.size()); - - incomplete_cells = 0; - - for(iCell = 0; iCell < cells.size(); iCell++){ - areaCell.at(iCell) = 0.0; - - if(completeCellMask.at(iCell) == 1){ - for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ - iEdge = edgesOnCell.at(iCell).at(j); - - if(cellsOnEdge.at(iEdge).at(0) == iCell){ - vertex1 = verticesOnEdge.at(iEdge).at(0); - vertex2 = verticesOnEdge.at(iEdge).at(1); - } else { - vertex1 = verticesOnEdge.at(iEdge).at(1); - vertex2 = verticesOnEdge.at(iEdge).at(0); - } - - if(vertex1 != -1 && vertex2 != -1){ - vert_loc1 = vertices.at(vertex1); - vert_loc2 = vertices.at(vertex2); - - if(!spherical){ - vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); - areaCell.at(iCell) += planarTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); - } else { - areaCell.at(iCell) += sphericalTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); - } - } - } - } else { - incomplete_cells++; -#ifdef _DEBUG - cout << " Non complete cell found at " << iCell << endl; -#endif - areaCell.at(iCell) = -1; - } - } - - for(iVertex = 0; iVertex < vertices.size(); iVertex++){ - areaTriangle.at(iVertex) = 0.0; - kiteAreasOnVertex.at(iVertex).resize(cellsOnVertex.at(iVertex).size()); - for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ - kiteAreasOnVertex.at(iVertex).at(j) = 0.0; - iCell = cellsOnVertex.at(iVertex).at(j); - - if(iCell != -1){ - edge1 = edgesOnVertex.at(iVertex).at(j); - - if(j == cellsOnVertex.at(iVertex).size()-1){ - edge2 = edgesOnVertex.at(iVertex).at(0); - } else { - edge2 = edgesOnVertex.at(iVertex).at(j+1); - } - - cell_loc = cells.at(iCell); - edge_loc1 = edges.at(edge1); - edge_loc2 = edges.at(edge2); - - if(!spherical){ - cell_loc.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); - edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); - edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); - kiteAreasOnVertex.at(iVertex).at(j) += + /* + * buildAreas constructs the area arrays. + * This includes areaCell, areaTriangle, and kiteAreasOnVertex + * + * Before constructing areaCell, the cell is checked for completeness. + * This is accomplished by looping over each edge. For each edge, + * the angle between it's vertices is computed and added to a running total. + * If the total is significantly less than 2*Pi, the cell is not complete. + * + * Non-complete cells are given a negative area, to ease removal at a later stage. + */ + int iVertex, iCell, iEdge, i, j; + int vertex1, vertex2; + int edge1, edge2; + int incomplete_cells; + pnt vert_loc1, vert_loc2; + pnt edge_loc1, edge_loc2; + pnt cell_loc; + pnt vec1, vec2; + +#ifdef _DEBUG + cout << endl << endl << "Begin function: buildAreas" << endl << endl; +#endif + + areaCell.clear(); + areaTriangle.clear(); + kiteAreasOnVertex.clear(); + + areaCell.resize(cells.size()); + areaTriangle.resize(vertices.size()); + kiteAreasOnVertex.resize(vertices.size()); + + incomplete_cells = 0; + + for(iCell = 0; iCell < cells.size(); iCell++){ + areaCell.at(iCell) = 0.0; + + if(completeCellMask.at(iCell) == 1){ + for(j = 0; j < edgesOnCell.at(iCell).size(); j++){ + iEdge = edgesOnCell.at(iCell).at(j); + + if(cellsOnEdge.at(iEdge).at(0) == iCell){ + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); + } else { + vertex1 = verticesOnEdge.at(iEdge).at(1); + vertex2 = verticesOnEdge.at(iEdge).at(0); + } + + if(vertex1 != -1 && vertex2 != -1){ + vert_loc1 = vertices.at(vertex1); + vert_loc2 = vertices.at(vertex2); + + if(!spherical){ + vert_loc1.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + vert_loc2.fixPeriodicity(cells.at(iCell), xPeriodicFix, yPeriodicFix); + areaCell.at(iCell) += planarTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); + } else { + areaCell.at(iCell) += sphericalTriangleArea(cells.at(iCell), vert_loc1, vert_loc2); + } + } + } + } else { + incomplete_cells++; +#ifdef _DEBUG + cout << " Non complete cell found at " << iCell << endl; +#endif + areaCell.at(iCell) = -1; + } + } + + for(iVertex = 0; iVertex < vertices.size(); iVertex++){ + areaTriangle.at(iVertex) = 0.0; + kiteAreasOnVertex.at(iVertex).resize(cellsOnVertex.at(iVertex).size()); + for(j = 0; j < cellsOnVertex.at(iVertex).size(); j++){ + kiteAreasOnVertex.at(iVertex).at(j) = 0.0; + iCell = cellsOnVertex.at(iVertex).at(j); + + if(iCell != -1){ + edge1 = edgesOnVertex.at(iVertex).at(j); + + if(j == cellsOnVertex.at(iVertex).size()-1){ + edge2 = edgesOnVertex.at(iVertex).at(0); + } else { + edge2 = edgesOnVertex.at(iVertex).at(j+1); + } + + cell_loc = cells.at(iCell); + edge_loc1 = edges.at(edge1); + edge_loc2 = edges.at(edge2); + + if(!spherical){ + cell_loc.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + edge_loc1.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + edge_loc2.fixPeriodicity(vertices.at(iVertex), xPeriodicFix, yPeriodicFix); + kiteAreasOnVertex.at(iVertex).at(j) += planarTriangleArea(vertices.at(iVertex), edge_loc1, cell_loc); - kiteAreasOnVertex.at(iVertex).at(j) += + kiteAreasOnVertex.at(iVertex).at(j) += planarTriangleArea(vertices.at(iVertex), cell_loc, edge_loc2); - } else { - kiteAreasOnVertex.at(iVertex).at(j) += + } else { + kiteAreasOnVertex.at(iVertex).at(j) += sphericalTriangleArea(vertices.at(iVertex), edge_loc1, cell_loc); - kiteAreasOnVertex.at(iVertex).at(j) += + kiteAreasOnVertex.at(iVertex).at(j) += sphericalTriangleArea(vertices.at(iVertex), cell_loc, edge_loc2); - } + } - areaTriangle.at(iVertex) += kiteAreasOnVertex.at(iVertex).at(j); - } - } - } + areaTriangle.at(iVertex) += kiteAreasOnVertex.at(iVertex).at(j); + } + } + } - cout << " Found " << incomplete_cells << " incomplete cells. Each is marked with an area of -1." << endl; + cout << " Found " << incomplete_cells << " incomplete cells. Each is marked with an area of -1." << endl; - return 0; + return 0; }/*}}}*/ int buildEdgesOnEdgeArrays(){/*{{{*/ - /* - * buildEdgesOnEdgeArrays builds both edgesOnEdge and weightsOnEdge - * - * The weights correspond to edgesOnEdge and allow MPAS to reconstruct the - * edge perpendicular (previously defined as v) velocity using the edge - * neighbors - * - * Weight formulation is defined in J. Thurburn, et al. JCP 2009 - * Numerical representation of geostrophic modes on arbitrarily - * structured C-grids - * - * Before using this function, dcEdge, dvEdge, areaCell, and kiteAreasOnVertex - * all need to be computed correctly. - */ - int iEdge, iCell, cell1, cell2; - int i, j, k; - int shared_vertex; - int last_edge, cur_edge; - double area_sum; - bool found; + /* + * buildEdgesOnEdgeArrays builds both edgesOnEdge and weightsOnEdge + * + * The weights correspond to edgesOnEdge and allow MPAS to reconstruct the + * edge perpendicular (previously defined as v) velocity using the edge + * neighbors + * + * Weight formulation is defined in J. Thurburn, et al. JCP 2009 + * Numerical representation of geostrophic modes on arbitrarily + * structured C-grids + * + * Before using this function, dcEdge, dvEdge, areaCell, and kiteAreasOnVertex + * all need to be computed correctly. + */ + int iEdge, iCell, cell1, cell2; + int i, j, k; + int shared_vertex; + int last_edge, cur_edge; + double area_sum; + bool found; #ifdef _DEBUG - cout << endl << endl << "Begin function: buildEdgesOnEdgeArrays" << endl << endl; + cout << endl << endl << "Begin function: buildEdgesOnEdgeArrays" << endl << endl; #endif - edgesOnEdge.clear(); - edgesOnEdge.resize(edges.size()); + edgesOnEdge.clear(); + edgesOnEdge.resize(edges.size()); - weightsOnEdge.clear(); - weightsOnEdge.resize(edges.size()); + weightsOnEdge.clear(); + weightsOnEdge.resize(edges.size()); - for(iEdge = 0; iEdge < edges.size(); iEdge++){ + for(iEdge = 0; iEdge < edges.size(); iEdge++){ #ifdef _DEBUG - cout << "New edge: " << edges.at(iEdge) << endl; + cout << "New edge: " << edges.at(iEdge) << endl; #endif - cell1 = cellsOnEdge.at(iEdge).at(0); - cell2 = cellsOnEdge.at(iEdge).at(1); - found = false; + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); + found = false; - // Loop over cell 1. Starting from the edge after the current edge, add - // all edges counter clockwise around the cell. Don't add the current - // edge to the list. + // Loop over cell 1. Starting from the edge after the current edge, add + // all edges counter clockwise around the cell. Don't add the current + // edge to the list. #ifdef _DEBUG - cout << " On cell1: " << cell1 << endl; + cout << " On cell1: " << cell1 << endl; #endif - last_edge = iEdge; - area_sum = 0; - for(i = 0; i < edgesOnCell.at(cell1).size(); i++){ + last_edge = iEdge; + area_sum = 0; + for(i = 0; i < edgesOnCell.at(cell1).size(); i++){ #ifdef _DEBUG - cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; + cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; #endif - if(edgesOnCell.at(cell1).at(i) == iEdge){ - found = true; + if(edgesOnCell.at(cell1).at(i) == iEdge){ + found = true; #ifdef _DEBUG - cout << " -- found -- " << endl; + cout << " -- found -- " << endl; #endif - } + } - if(found && edgesOnCell.at(cell1).at(i) != iEdge){ - cur_edge = edgesOnCell.at(cell1).at(i); - edgesOnEdge.at(iEdge).push_back(cur_edge); + if(found && edgesOnCell.at(cell1).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell1).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); - // Find shared vertex between newly added edge - // and last_edge - if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(0); + shared_vertex = verticesOnEdge.at(last_edge).at(0); - } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(1); - } + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } - if(shared_vertex != -1) { - // Find cell 1 on shared vertex (to get kite area) - for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ - iCell = cellsOnVertex.at(shared_vertex).at(j); + if(shared_vertex != -1) { + // Find cell 1 on shared vertex (to get kite area) + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); - if(iCell == cell1){ - area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); - } - } - } + if(iCell == cell1){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); + } + } + } - if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ - weightsOnEdge.at(iEdge).push_back( + if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } else { - weightsOnEdge.at(iEdge).push_back( + } else { + weightsOnEdge.at(iEdge).push_back( -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } + } - last_edge = edgesOnCell.at(cell1).at(i); + last_edge = edgesOnCell.at(cell1).at(i); #ifdef _DEBUG - cout << " added " << edgesOnCell.at(cell1).at(i) << endl; + cout << " added " << edgesOnCell.at(cell1).at(i) << endl; #endif - } - } - if(!found) { - cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; - return 1; - } + } + } + if(!found) { + cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; + return 1; + } - for(i = 0; i < edgesOnCell.at(cell1).size() && found; i++){ + for(i = 0; i < edgesOnCell.at(cell1).size() && found; i++){ #ifdef _DEBUG - cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; + cout << " checking edge: " << edgesOnCell.at(cell1).at(i) << endl; #endif - if(edgesOnCell.at(cell1).at(i) == iEdge){ + if(edgesOnCell.at(cell1).at(i) == iEdge){ #ifdef _DEBUG - cout << " -- found -- " << endl; + cout << " -- found -- " << endl; #endif - found = false; - } + found = false; + } - if(found && edgesOnCell.at(cell1).at(i) != iEdge){ - cur_edge = edgesOnCell.at(cell1).at(i); - edgesOnEdge.at(iEdge).push_back(cur_edge); + if(found && edgesOnCell.at(cell1).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell1).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); - // Find shared vertex between newly added edge - // and last_edge - if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(0); + shared_vertex = verticesOnEdge.at(last_edge).at(0); - } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(1); - } + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } - // Find cell 1 on shared vertex (to get kite area) - if(shared_vertex != -1){ - for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ - iCell = cellsOnVertex.at(shared_vertex).at(j); + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); - if(iCell == cell1){ - area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); - } - } - } + if(iCell == cell1){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell1); + } + } + } - if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ - weightsOnEdge.at(iEdge).push_back( + if(cell1 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } else { - weightsOnEdge.at(iEdge).push_back( + } else { + weightsOnEdge.at(iEdge).push_back( -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } + } - last_edge = edgesOnCell.at(cell1).at(i); + last_edge = edgesOnCell.at(cell1).at(i); #ifdef _DEBUG - cout << " added " << edgesOnCell.at(cell1).at(i) << endl; + cout << " added " << edgesOnCell.at(cell1).at(i) << endl; #endif - } - } - if(found) { - cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; - return 1; - } + } + } + if(found) { + cout << "Error finding edge " << iEdge << " on cell " << cell1 << " 1" << endl; + return 1; + } - // Check if cell1 is a real cell or not. - if(cell2 > -1){ - last_edge = iEdge; - area_sum = 0.0; + // Check if cell1 is a real cell or not. + if(cell2 > -1){ + last_edge = iEdge; + area_sum = 0.0; #ifdef _DEBUG - cout << " On cell2: " << cell2 << endl; + cout << " On cell2: " << cell2 << endl; #endif - found = false; - // Loop over cell 2. Starting from the edge after the current edge, - // add all edges counter clockwise around the cell. Don't add the - // current edge to the list. - for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ + found = false; + // Loop over cell 2. Starting from the edge after the current edge, + // add all edges counter clockwise around the cell. Don't add the + // current edge to the list. + for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ #ifdef _DEBUG - cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; + cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; #endif - if(edgesOnCell.at(cell2).at(i) == iEdge){ - found = true; + if(edgesOnCell.at(cell2).at(i) == iEdge){ + found = true; #ifdef _DEBUG - cout << " -- found -- " << endl; + cout << " -- found -- " << endl; #endif - } + } - if(found && edgesOnCell.at(cell2).at(i) != iEdge){ - cur_edge = edgesOnCell.at(cell2).at(i); - edgesOnEdge.at(iEdge).push_back(cur_edge); + if(found && edgesOnCell.at(cell2).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell2).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); - // Find shared vertex between newly added edge - // and last_edge - if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(0); + shared_vertex = verticesOnEdge.at(last_edge).at(0); - } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(1); - } + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } - // Find cell 1 on shared vertex (to get kite area) - if(shared_vertex != -1){ - for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ - iCell = cellsOnVertex.at(shared_vertex).at(j); + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); - if(iCell == cell2){ - area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); - } - } - } + if(iCell == cell2){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); + } + } + } - if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ - weightsOnEdge.at(iEdge).push_back( + if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } else { - weightsOnEdge.at(iEdge).push_back( + } else { + weightsOnEdge.at(iEdge).push_back( +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } + } - last_edge = edgesOnCell.at(cell2).at(i); + last_edge = edgesOnCell.at(cell2).at(i); #ifdef _DEBUG - cout << " added " << edgesOnCell.at(cell2).at(i) << endl; + cout << " added " << edgesOnCell.at(cell2).at(i) << endl; #endif - } - } - if(!found) { - cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; - return 1; - } + } + } + if(!found) { + cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; + return 1; + } - for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ + for(i = 0; i < edgesOnCell.at(cell2).size(); i++){ #ifdef _DEBUG - cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; + cout << " checking edge: " << edgesOnCell.at(cell2).at(i) << endl; #endif - if(edgesOnCell.at(cell2).at(i) == iEdge){ - found = false; + if(edgesOnCell.at(cell2).at(i) == iEdge){ + found = false; #ifdef _DEBUG - cout << " -- found -- " << endl; + cout << " -- found -- " << endl; #endif - } + } - if(found && edgesOnCell.at(cell2).at(i) != iEdge){ - cur_edge = edgesOnCell.at(cell2).at(i); - edgesOnEdge.at(iEdge).push_back(cur_edge); + if(found && edgesOnCell.at(cell2).at(i) != iEdge){ + cur_edge = edgesOnCell.at(cell2).at(i); + edgesOnEdge.at(iEdge).push_back(cur_edge); - // Find shared vertex between newly added edge - // and last_edge - if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ + // Find shared vertex between newly added edge + // and last_edge + if(verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(0) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(0); + shared_vertex = verticesOnEdge.at(last_edge).at(0); - } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || - verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ + } else if(verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(0) || + verticesOnEdge.at(last_edge).at(1) == verticesOnEdge.at(cur_edge).at(1)){ - shared_vertex = verticesOnEdge.at(last_edge).at(1); - } + shared_vertex = verticesOnEdge.at(last_edge).at(1); + } - // Find cell 1 on shared vertex (to get kite area) - if(shared_vertex != -1){ - for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ - iCell = cellsOnVertex.at(shared_vertex).at(j); + // Find cell 1 on shared vertex (to get kite area) + if(shared_vertex != -1){ + for(j = 0; j < cellsOnVertex.at(shared_vertex).size(); j++){ + iCell = cellsOnVertex.at(shared_vertex).at(j); - if(iCell == cell2){ - area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); - } - } - } + if(iCell == cell2){ + area_sum += kiteAreasOnVertex.at(shared_vertex).at(j) / areaCell.at(cell2); + } + } + } - if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ - weightsOnEdge.at(iEdge).push_back( + if(cell2 == cellsOnEdge.at(cur_edge).at(0)){ + weightsOnEdge.at(iEdge).push_back( -1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } else { - weightsOnEdge.at(iEdge).push_back( + } else { + weightsOnEdge.at(iEdge).push_back( +1.0 * (0.5 - area_sum) * dvEdge.at(cur_edge) / dcEdge.at(iEdge) ); - } + } - last_edge = edgesOnCell.at(cell2).at(i); + last_edge = edgesOnCell.at(cell2).at(i); #ifdef _DEBUG - cout << " added " << edgesOnCell.at(cell2).at(i) << endl; + cout << " added " << edgesOnCell.at(cell2).at(i) << endl; #endif - } - } - if(found) { - cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; - return 1; - } - } - } + } + } + if(found) { + cout << "Error finding edge " << iEdge << " on cell " << cell2 << " 2" << endl; + return 1; + } + } + } - return 0; + return 0; }/*}}}*/ int buildAngleEdge(){/*{{{*/ - /* - * buildAngleEdge constructs angle edge for each edge. - * - * angleEdge is either: - * 1. The angle the positive tangential direction (v) - * makes with the local northward direction. - * or - * 2. The angles the positive normal direction (u) - * makes with the local eastward direction. - * - * In a plane, local eastward is defined as the x axis, and - * nortward is defined as the y axis. - */ - int iEdge; - int cell1, cell2; - int vertex1, vertex2; - pnt np, x_axis, normal; - pnt cell_loc1, cell_loc2; - pnt vertex_loc1, vertex_loc2; - double angle, sign; + /* + * buildAngleEdge constructs angle edge for each edge. + * + * angleEdge is either: + * 1. The angle the positive tangential direction (v) + * makes with the local northward direction. + * or + * 2. The angles the positive normal direction (u) + * makes with the local eastward direction. + * + * In a plane, local eastward is defined as the x axis, and + * nortward is defined as the y axis. + */ + int iEdge; + int cell1, cell2; + int vertex1, vertex2; + pnt np, x_axis, normal; + pnt cell_loc1, cell_loc2; + pnt vertex_loc1, vertex_loc2; + double angle, sign; #ifdef _DEBUG - cout << endl << endl << "Begin function: buildAngleEdge" << endl << endl; + cout << endl << endl << "Begin function: buildAngleEdge" << endl << endl; #endif - angleEdge.clear(); - angleEdge.resize(edges.size()); + angleEdge.clear(); + angleEdge.resize(edges.size()); - x_axis = pnt(1.0, 0.0, 0.0); + x_axis = pnt(1.0, 0.0, 0.0); - for(iEdge = 0; iEdge < edges.size(); iEdge++){ + for(iEdge = 0; iEdge < edges.size(); iEdge++){ #ifdef _DEBUG - cout << "New edge: " << edges.at(iEdge) << endl; + cout << "New edge: " << edges.at(iEdge) << endl; #endif - cell1 = cellsOnEdge.at(iEdge).at(0); - cell2 = cellsOnEdge.at(iEdge).at(1); + cell1 = cellsOnEdge.at(iEdge).at(0); + cell2 = cellsOnEdge.at(iEdge).at(1); - vertex1 = verticesOnEdge.at(iEdge).at(0); - vertex2 = verticesOnEdge.at(iEdge).at(1); + vertex1 = verticesOnEdge.at(iEdge).at(0); + vertex2 = verticesOnEdge.at(iEdge).at(1); - cell_loc1 = cells.at(cell1); - if(cell2 != -1){ - cell_loc2 = cells.at(cell2); - } else { - cell_loc2 = edges.at(iEdge); - } + cell_loc1 = cells.at(cell1); + if(cell2 != -1){ + cell_loc2 = cells.at(cell2); + } else { + cell_loc2 = edges.at(iEdge); + } - vertex_loc1 = vertices.at(vertex1); - if(vertex2 != -1){ - vertex_loc2 = vertices.at(vertex2); - } else { - vertex_loc2 = edges.at(iEdge); - } + vertex_loc1 = vertices.at(vertex1); + if(vertex2 != -1){ + vertex_loc2 = vertices.at(vertex2); + } else { + vertex_loc2 = edges.at(iEdge); + } - if(!spherical){ - cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); + if(!spherical){ + cell_loc2.fixPeriodicity(cell_loc1, xPeriodicFix, yPeriodicFix); - normal = cell_loc2 - cell_loc1; - angleEdge.at(iEdge) = acos( x_axis.dot(normal) / (x_axis.magnitude() * normal.magnitude())); - if (cell_loc2.y < cell_loc1.y) angleEdge.at(iEdge) = 2.0 * M_PI - angleEdge.at(iEdge); - } else { + normal = cell_loc2 - cell_loc1; + angleEdge.at(iEdge) = acos( x_axis.dot(normal) / (x_axis.magnitude() * normal.magnitude())); + if (cell_loc2.y < cell_loc1.y) angleEdge.at(iEdge) = 2.0 * M_PI - angleEdge.at(iEdge); + } else { - np = pntFromLatLon(edges.at(iEdge).getLat()+0.05, edges.at(iEdge).getLon()); - np.normalize(); + np = pntFromLatLon(edges.at(iEdge).getLat()+0.05, edges.at(iEdge).getLon()); + np.normalize(); #ifdef _DEBUG - cout << " NP: " << np << endl; + cout << " NP: " << np << endl; #endif - angle = (vertex_loc2.getLat() - vertex_loc1.getLat()) / dvEdge.at(iEdge); - angle = max( min(angle, 1.0), -1.0); - angle = acos(angle); + angle = (vertex_loc2.getLat() - vertex_loc1.getLat()) / dvEdge.at(iEdge); + angle = max( min(angle, 1.0), -1.0); + angle = acos(angle); #ifdef _DEBUG - cout << " angle: " << angle << endl; + cout << " angle: " << angle << endl; #endif - sign = planeAngle(edges.at(iEdge), np, vertex_loc2, edges.at(iEdge)); - if(sign != 0.0){ - sign = sign / fabs(sign); - } else { - sign = 1.0; - } + sign = planeAngle(edges.at(iEdge), np, vertex_loc2, edges.at(iEdge)); + if(sign != 0.0){ + sign = sign / fabs(sign); + } else { + sign = 1.0; + } #ifdef _DEBUG - cout << " sign : " << sign << endl; - cout << " a*s : " << angle * sign << endl; + cout << " sign : " << sign << endl; + cout << " a*s : " << angle * sign << endl; #endif - angle = angle * sign; - if(angle > M_PI) angle = angle - 2.0 * M_PI; - if(angle < -M_PI) angle = angle + 2.0 * M_PI; + angle = angle * sign; + if(angle > M_PI) angle = angle - 2.0 * M_PI; + if(angle < -M_PI) angle = angle + 2.0 * M_PI; #ifdef _DEBUG - cout << " fangle: " << angle << endl; + cout << " fangle: " << angle << endl; #endif - angleEdge.at(iEdge) = angle; - } - } + angleEdge.at(iEdge) = angle; + } + } - return 0; + return 0; }/*}}}*/ int buildMeshQualities(){/*{{{*/ - /* - * buildMeshQualities constructs fields describing the quality of the mesh, including: - * - cellQuality: a double between 0 and 1 describing how uniform the cell is - * - gridSpacing: an estimate of the grid spacing of each cell - * - triangleQuality: a double between 0 and 1 describing how uniform the cell is based on edge lenghts - * - triangleAngleQuality: a double between 0 and 1 describing how uniform the cell is based on the angles of the triangle - * - obtuseTriangle: an integer either 0 or 1 that flags triangles with an obtuse angle (set to 1) - */ - - int iCell, iVertex, iEdge; - int i, j; - double maxEdge, minEdge, spacing; - double angle, maxAngle, minAngle; - int obtuse; - - cellQuality.clear(); - gridSpacing.clear(); - triangleQuality.clear(); - triangleAngleQuality.clear(); - obtuseTriangle.clear(); - - cellQuality.resize(cells.size()); - gridSpacing.resize(cells.size()); - - triangleQuality.resize(vertices.size()); - triangleAngleQuality.resize(vertices.size()); - obtuseTriangle.resize(vertices.size()); - -#ifdef _DEBUG - cout << "Starting mesh quality calcs." < 0.0) { - cellQuality.at(iCell) = minEdge / maxEdge; - } else { - cellQuality.at(iCell) = 0.0; - } - gridSpacing.at(iCell) = spacing / edgesOnCell.at(iCell).size(); - } + /* + * buildMeshQualities constructs fields describing the quality of the mesh, including: + * - cellQuality: a double between 0 and 1 describing how uniform the cell is + * - gridSpacing: an estimate of the grid spacing of each cell + * - triangleQuality: a double between 0 and 1 describing how uniform the cell is based on edge lenghts + * - triangleAngleQuality: a double between 0 and 1 describing how uniform the cell is based on the angles of the triangle + * - obtuseTriangle: an integer either 0 or 1 that flags triangles with an obtuse angle (set to 1) + */ + + int iCell, iVertex, iEdge; + int i, j; + double maxEdge, minEdge, spacing; + double angle, maxAngle, minAngle; + int obtuse; + + cellQuality.clear(); + gridSpacing.clear(); + triangleQuality.clear(); + triangleAngleQuality.clear(); + obtuseTriangle.clear(); + + cellQuality.resize(cells.size()); + gridSpacing.resize(cells.size()); + + triangleQuality.resize(vertices.size()); + triangleAngleQuality.resize(vertices.size()); + obtuseTriangle.resize(vertices.size()); + +#ifdef _DEBUG + cout << "Starting mesh quality calcs." < 0.0) { + cellQuality.at(iCell) = minEdge / maxEdge; + } else { + cellQuality.at(iCell) = 0.0; + } + gridSpacing.at(iCell) = spacing / edgesOnCell.at(iCell).size(); + } #ifdef _DEBUG - cout << "Starting loop over vertices." < M_PI_2 ) { - obtuse = 1; - obtuseTriangles++; - } - - triangleAngleQuality.at(iVertex) = minAngle / maxAngle; - obtuseTriangle.at(iVertex) = obtuse; - } else { - triangleAngleQuality.at(iVertex) = 1.0; - obtuseTriangle.at(iVertex) = 0; - } - } - } - } + minAngle = min(angle1, min(angle2, angle3)); + maxAngle = max(angle1, max(angle2, angle3)); + + if ( maxAngle > M_PI_2 ) { + obtuse = 1; + obtuseTriangles++; + } + + triangleAngleQuality.at(iVertex) = minAngle / maxAngle; + obtuseTriangle.at(iVertex) = obtuse; + } else { + triangleAngleQuality.at(iVertex) = 1.0; + obtuseTriangle.at(iVertex) = 0; + } + } + } + } - cout << "\tMesh contains: " << obtuseTriangles << " obtuse triangles." << endl; + cout << "\tMesh contains: " << obtuseTriangles << " obtuse triangles." << endl; - return 0; + return 0; }/*}}}*/ /*}}}*/ /* Output functions {{{*/ int outputGridDimensions( const string outputFilename ){/*{{{*/ - /************************************************************************ - * - * This function writes the grid dimensions to the netcdf file named - * outputFilename - * - * **********************************************************************/ - - int grid, retv; + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ + + int grid, retv; if ((retv = nc_create(outputFilename.c_str(), NC_CLOBBER|NC_NETCDF4, &grid))) { std::cout << "Can't create file: " << outputFilename << std::endl; return retv ; } - nCells = cells.size(); + nCells = cells.size(); + + /* + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + maxEdges = std::max(maxEdges, (int)(*vec_int_itr).size()); + }*/ - /* - for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ - maxEdges = std::max(maxEdges, (int)(*vec_int_itr).size()); - }*/ - ncutil::def_dim(outputFilename, "nCells", cells.size()); ncutil::def_dim(outputFilename, "nEdges", edges.size()); ncutil::def_dim(outputFilename, "nVertices", vertices.size()); @@ -2401,60 +2401,60 @@ int outputGridDimensions( const string outputFilename ){/*{{{*/ ncutil::def_dim(outputFilename, "vertexDegree", vertex_degree); ncutil::def_dim(outputFilename, "Time", NC_UNLIMITED); - return 0; + return 0; }/*}}}*/ int outputGridAttributes( const string outputFilename, const string inputFilename ){/*{{{*/ - /************************************************************************ - * - * This function writes the grid dimensions to the netcdf file named - * outputFilename - * - * **********************************************************************/ + /************************************************************************ + * + * This function writes the grid dimensions to the netcdf file named + * outputFilename + * + * **********************************************************************/ - char mesh_spec_str[1024]; + char mesh_spec_str[1024]; - string history_str = ""; - string id_str = ""; - string parent_str =""; + string history_str = ""; + string id_str = ""; + string parent_str =""; - // write attributes - if(!spherical){ + // write attributes + if(!spherical){ ncutil::put_str(outputFilename, "on_a_sphere", "NO"); ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, 0.); - } else { + } else { ncutil::put_str(outputFilename, "on_a_sphere", "YES"); - ncutil::put_att(outputFilename, + ncutil::put_att(outputFilename, "sphere_radius", NC_DOUBLE, sphereRadius); - } + } - if(!periodic){ + if(!periodic){ ncutil::put_str(outputFilename, "is_periodic", "NO"); - } else { + } else { ncutil::put_str(outputFilename, "is_periodic", "YES"); ncutil::put_att(outputFilename, "x_period", NC_DOUBLE, xPeriod); ncutil::put_att(outputFilename, "y_period", NC_DOUBLE, yPeriod); - } - - history_str += "MpasMeshConverter.x "; - history_str += inputFilename; - history_str += " "; - history_str += outputFilename; - if(in_history != ""){ - history_str += "\n"; - history_str += in_history; - } - - if(in_file_id != "" ){ - parent_str = in_file_id; - if(in_parent_id != ""){ - parent_str += "\n"; - parent_str += in_parent_id; - } + } + + history_str += "MpasMeshConverter.x "; + history_str += inputFilename; + history_str += " "; + history_str += outputFilename; + if(in_history != ""){ + history_str += "\n"; + history_str += in_history; + } + + if(in_file_id != "" ){ + parent_str = in_file_id; + if(in_parent_id != ""){ + parent_str += "\n"; + parent_str += in_parent_id; + } ncutil::put_str(outputFilename, "parent_id", parent_str); - } - id_str = gen_random(ID_LEN); + } + id_str = gen_random(ID_LEN); - sprintf(mesh_spec_str, "%2.1lf", (double)MESH_SPEC); + sprintf(mesh_spec_str, "%2.1lf", (double)MESH_SPEC); ncutil::put_str(outputFilename, "history", history_str); ncutil::put_str(outputFilename, "mesh_spec", mesh_spec_str); @@ -2462,619 +2462,619 @@ int outputGridAttributes( const string outputFilename, const string inputFilenam ncutil::put_str(outputFilename, "source", "MpasMeshConverter.x"); ncutil::put_str(outputFilename, "file_id", id_str); - return 0; + return 0; }/*}}}*/ int outputGridCoordinates( const string outputFilename) {/*{{{*/ - /************************************************************************ - * - * This function writes the grid coordinates to the netcdf file named - * outputFilename - * This includes all cell centers, vertices, and edges. - * Both cartesian and lat,lon, as well as all of their indices - * - * **********************************************************************/ - - int nCells = cells.size(); - int nEdges = edges.size(); - int nVertices = vertices.size(); - - int i; - double *x, *y, *z, *lat, *lon; - int *idxTo; - - // Build and write cell coordinate arrays - x = new double[nCells]; - y = new double[nCells]; - z = new double[nCells]; - lat = new double[nCells]; - lon = new double[nCells]; - idxTo = new int[nCells]; - i = 0; - for(pnt_itr = cells.begin(); pnt_itr != cells.end(); ++pnt_itr){ - if(!spherical){ - x[i] = (*pnt_itr).x; - y[i] = (*pnt_itr).y; - z[i] = (*pnt_itr).z; - lat[i] = 0.0; - lon[i] = 0.0; - } else { - x[i] = (*pnt_itr).x * sphereRadius; - y[i] = (*pnt_itr).y * sphereRadius; - z[i] = (*pnt_itr).z * sphereRadius; - lat[i] = (*pnt_itr).getLat(); - lon[i] = (*pnt_itr).getLon(); - } - idxTo[i] = (*pnt_itr).idx+1; - - i++; - } - - ncutil::def_var(outputFilename, "latCell", + /************************************************************************ + * + * This function writes the grid coordinates to the netcdf file named + * outputFilename + * This includes all cell centers, vertices, and edges. + * Both cartesian and lat,lon, as well as all of their indices + * + * **********************************************************************/ + + int nCells = cells.size(); + int nEdges = edges.size(); + int nVertices = vertices.size(); + + int i; + double *x, *y, *z, *lat, *lon; + int *idxTo; + + // Build and write cell coordinate arrays + x = new double[nCells]; + y = new double[nCells]; + z = new double[nCells]; + lat = new double[nCells]; + lon = new double[nCells]; + idxTo = new int[nCells]; + i = 0; + for(pnt_itr = cells.begin(); pnt_itr != cells.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latCell", NC_DOUBLE, "latitudes of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "lonCell", + ncutil::def_var(outputFilename, "lonCell", NC_DOUBLE, "longitudes of cell centres", {"nCells"}); - ncutil::put_var(outputFilename, "latCell", &lat[0]); + ncutil::put_var(outputFilename, "latCell", &lat[0]); ncutil::put_var(outputFilename, "lonCell", &lon[0]); - ncutil::def_var(outputFilename, "xCell", + ncutil::def_var(outputFilename, "xCell", NC_DOUBLE, "x-coordinates of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "yCell", + ncutil::def_var(outputFilename, "yCell", NC_DOUBLE, "y-coordinates of cell centres", {"nCells"}); - ncutil::def_var(outputFilename, "zCell", + ncutil::def_var(outputFilename, "zCell", NC_DOUBLE, "z-coordinates of cell centres", {"nCells"}); - ncutil::put_var(outputFilename, "xCell", &x[0]); + ncutil::put_var(outputFilename, "xCell", &x[0]); ncutil::put_var(outputFilename, "yCell", &y[0]); ncutil::put_var(outputFilename, "zCell", &z[0]); - ncutil::def_var(outputFilename, "indexToCellID", + ncutil::def_var(outputFilename, "indexToCellID", NC_INT, "index to cell ID mapping", {"nCells"}); ncutil::put_var(outputFilename, "indexToCellID", &idxTo[0]); - delete[] x; - delete[] y; - delete[] z; - delete[] lat; - delete[] lon; - delete[] idxTo; - - //Build and write edge coordinate arrays - x = new double[nEdges]; - y = new double[nEdges]; - z = new double[nEdges]; - lat = new double[nEdges]; - lon = new double[nEdges]; - idxTo = new int[nEdges]; - - i = 0; - for(pnt_itr = edges.begin(); pnt_itr != edges.end(); ++pnt_itr){ - if(!spherical){ - x[i] = (*pnt_itr).x; - y[i] = (*pnt_itr).y; - z[i] = (*pnt_itr).z; - lat[i] = 0.0; - lon[i] = 0.0; - } else { - x[i] = (*pnt_itr).x * sphereRadius; - y[i] = (*pnt_itr).y * sphereRadius; - z[i] = (*pnt_itr).z * sphereRadius; - lat[i] = (*pnt_itr).getLat(); - lon[i] = (*pnt_itr).getLon(); - } - idxTo[i] = (*pnt_itr).idx+1; - - i++; - } - - ncutil::def_var(outputFilename, "latEdge", + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; + + //Build and write edge coordinate arrays + x = new double[nEdges]; + y = new double[nEdges]; + z = new double[nEdges]; + lat = new double[nEdges]; + lon = new double[nEdges]; + idxTo = new int[nEdges]; + + i = 0; + for(pnt_itr = edges.begin(); pnt_itr != edges.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latEdge", NC_DOUBLE, "latitudes of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "lonEdge", + ncutil::def_var(outputFilename, "lonEdge", NC_DOUBLE, "longitudes of edge centres", {"nEdges"}); - ncutil::put_var(outputFilename, "latEdge", &lat[0]); + ncutil::put_var(outputFilename, "latEdge", &lat[0]); ncutil::put_var(outputFilename, "lonEdge", &lon[0]); - ncutil::def_var(outputFilename, "xEdge", + ncutil::def_var(outputFilename, "xEdge", NC_DOUBLE, "x-coordinates of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "yEdge", + ncutil::def_var(outputFilename, "yEdge", NC_DOUBLE, "y-coordinates of edge centres", {"nEdges"}); - ncutil::def_var(outputFilename, "zEdge", + ncutil::def_var(outputFilename, "zEdge", NC_DOUBLE, "z-coordinates of edge centres", {"nEdges"}); - ncutil::put_var(outputFilename, "xEdge", &x[0]); + ncutil::put_var(outputFilename, "xEdge", &x[0]); ncutil::put_var(outputFilename, "yEdge", &y[0]); ncutil::put_var(outputFilename, "zEdge", &z[0]); - ncutil::def_var(outputFilename, "indexToEdgeID", + ncutil::def_var(outputFilename, "indexToEdgeID", NC_INT, "index to edge ID mapping", {"nEdges"}); ncutil::put_var(outputFilename, "indexToEdgeID", &idxTo[0]); - delete[] x; - delete[] y; - delete[] z; - delete[] lat; - delete[] lon; - delete[] idxTo; - - //Build and write vertex coordinate arrays - x = new double[nVertices]; - y = new double[nVertices]; - z = new double[nVertices]; - lat = new double[nVertices]; - lon = new double[nVertices]; - idxTo = new int[nVertices]; - - i = 0; - for(pnt_itr = vertices.begin(); pnt_itr != vertices.end(); ++pnt_itr){ - if(!spherical){ - x[i] = (*pnt_itr).x; - y[i] = (*pnt_itr).y; - z[i] = (*pnt_itr).z; - lat[i] = 0.0; - lon[i] = 0.0; - } else { - x[i] = (*pnt_itr).x * sphereRadius; - y[i] = (*pnt_itr).y * sphereRadius; - z[i] = (*pnt_itr).z * sphereRadius; - lat[i] = (*pnt_itr).getLat(); - lon[i] = (*pnt_itr).getLon(); - } - idxTo[i] = (*pnt_itr).idx+1; - - i++; - } - - ncutil::def_var(outputFilename, "latVertex", + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; + + //Build and write vertex coordinate arrays + x = new double[nVertices]; + y = new double[nVertices]; + z = new double[nVertices]; + lat = new double[nVertices]; + lon = new double[nVertices]; + idxTo = new int[nVertices]; + + i = 0; + for(pnt_itr = vertices.begin(); pnt_itr != vertices.end(); ++pnt_itr){ + if(!spherical){ + x[i] = (*pnt_itr).x; + y[i] = (*pnt_itr).y; + z[i] = (*pnt_itr).z; + lat[i] = 0.0; + lon[i] = 0.0; + } else { + x[i] = (*pnt_itr).x * sphereRadius; + y[i] = (*pnt_itr).y * sphereRadius; + z[i] = (*pnt_itr).z * sphereRadius; + lat[i] = (*pnt_itr).getLat(); + lon[i] = (*pnt_itr).getLon(); + } + idxTo[i] = (*pnt_itr).idx+1; + + i++; + } + + ncutil::def_var(outputFilename, "latVertex", NC_DOUBLE, "latitudes of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "lonVertex", + ncutil::def_var(outputFilename, "lonVertex", NC_DOUBLE, "longitudes of vertices", {"nVertices"}); - ncutil::put_var(outputFilename, "latVertex", &lat[0]); + ncutil::put_var(outputFilename, "latVertex", &lat[0]); ncutil::put_var(outputFilename, "lonVertex", &lon[0]); - ncutil::def_var(outputFilename, "xVertex", + ncutil::def_var(outputFilename, "xVertex", NC_DOUBLE, "x-coordinates of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "yVertex", + ncutil::def_var(outputFilename, "yVertex", NC_DOUBLE, "y-coordinates of vertices", {"nVertices"}); - ncutil::def_var(outputFilename, "zVertex", + ncutil::def_var(outputFilename, "zVertex", NC_DOUBLE, "z-coordinates of vertices", {"nVertices"}); - ncutil::put_var(outputFilename, "xVertex", &x[0]); + ncutil::put_var(outputFilename, "xVertex", &x[0]); ncutil::put_var(outputFilename, "yVertex", &y[0]); ncutil::put_var(outputFilename, "zVertex", &z[0]); - ncutil::def_var(outputFilename, "indexToVertexID", + ncutil::def_var(outputFilename, "indexToVertexID", NC_INT, "index to vertex ID mapping", {"nVertices"}); ncutil::put_var(outputFilename, "indexToVertexID", &idxTo[0]); - delete[] x; - delete[] y; - delete[] z; - delete[] lat; - delete[] lon; - delete[] idxTo; + delete[] x; + delete[] y; + delete[] z; + delete[] lat; + delete[] lon; + delete[] idxTo; - return 0; + return 0; }/*}}}*/ int outputCellConnectivity( const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function writes all of the *OnCell arrays. Including - * cellsOnCell - * edgesOnCell - * verticesOnCell - * nEdgesonCell - * - * ***************************************************************/ - - int nCells = cells.size(); - int i, j; - - int *tmp_arr; - - // Build and write COC array - tmp_arr = new int[nCells*maxEdges]; - - for(i = 0; i < nCells; i++){ - for(j = 0; j < maxEdges; j++){ - tmp_arr[i*maxEdges + j] = 0; - } - } - - i = 0; - for(vec_int_itr = cellsOnCell.begin(); vec_int_itr != cellsOnCell.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*maxEdges + j] = (*int_itr) + 1; - j++; - } - i++; - } - - ncutil::def_var(outputFilename, "cellsOnCell", + /***************************************************************** + * + * This function writes all of the *OnCell arrays. Including + * cellsOnCell + * edgesOnCell + * verticesOnCell + * nEdgesonCell + * + * ***************************************************************/ + + int nCells = cells.size(); + int i, j; + + int *tmp_arr; + + // Build and write COC array + tmp_arr = new int[nCells*maxEdges]; + + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = cellsOnCell.begin(); vec_int_itr != cellsOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "cellsOnCell", NC_INT, "cells adj. to each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "cellsOnCell", &tmp_arr[0]); - // Build and write EOC array - for(i = 0; i < nCells; i++){ - for(j = 0; j < maxEdges; j++){ - tmp_arr[i*maxEdges + j] = 0; - } - } - - i = 0; - for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*maxEdges + j] = (*int_itr) + 1; - j++; - } - - i++; - } - - ncutil::def_var(outputFilename, "edgesOnCell", + // Build and write EOC array + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnCell", NC_INT, "edges on each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "edgesOnCell", &tmp_arr[0]); - // Build and write VOC array - for(i = 0; i < nCells; i++){ - for(j = 0; j < maxEdges; j++){ - tmp_arr[i*maxEdges + j] = 0; - } - } - - i = 0; - for(vec_int_itr = verticesOnCell.begin(); vec_int_itr != verticesOnCell.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*maxEdges + j] = (*int_itr) + 1; - j++; - } - i++; - } - - ncutil::def_var(outputFilename, "verticesOnCell", + // Build and write VOC array + for(i = 0; i < nCells; i++){ + for(j = 0; j < maxEdges; j++){ + tmp_arr[i*maxEdges + j] = 0; + } + } + + i = 0; + for(vec_int_itr = verticesOnCell.begin(); vec_int_itr != verticesOnCell.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "verticesOnCell", NC_INT, "vertices on each cell", {"nCells", "maxEdges"}); ncutil::put_var(outputFilename, "verticesOnCell", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - //Build and write nEOC array - tmp_arr = new int[nCells]; + //Build and write nEOC array + tmp_arr = new int[nCells]; - i = 0; - for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ - tmp_arr[i] = (*vec_int_itr).size(); - i++; - } + i = 0; + for(vec_int_itr = edgesOnCell.begin(); vec_int_itr != edgesOnCell.end(); ++vec_int_itr){ + tmp_arr[i] = (*vec_int_itr).size(); + i++; + } - ncutil::def_var(outputFilename, "nEdgesOnCell", + ncutil::def_var(outputFilename, "nEdgesOnCell", NC_INT, "number of edges on each cell", {"nCells"}); ncutil::put_var(outputFilename, "nEdgesOnCell", &tmp_arr[0]); - verticesOnCell.clear(); - edgesOnCell.clear(); + verticesOnCell.clear(); + edgesOnCell.clear(); - delete[] tmp_arr; + delete[] tmp_arr; - return 0; + return 0; }/*}}}*/ int outputEdgeConnectivity( const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function writes all of the *OnEdge arrays. Including - * cellsOnEdge - * edgesOnEdge - * verticesOnEdge - * nEdgesOnEdge - * - * ***************************************************************/ - + /***************************************************************** + * + * This function writes all of the *OnEdge arrays. Including + * cellsOnEdge + * edgesOnEdge + * verticesOnEdge + * nEdgesOnEdge + * + * ***************************************************************/ + int nEdges = edges.size(); - int maxEdges2 = maxEdges * 2; - int vertexDegree = vertex_degree; - int two = 2; - int i, j; - - int *tmp_arr; - - // Build and write EOE array - tmp_arr = new int[nEdges*maxEdges2]; - - for(i = 0; i < nEdges; i++){ - for(j = 0; j < maxEdges2; j++){ - tmp_arr[i*maxEdges2 + j] = 0; - } - } - - i = 0; - for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*maxEdges2 + j] = (*int_itr) + 1; - j++; - } - - i++; - } - - ncutil::def_var(outputFilename, "edgesOnEdge", + int maxEdges2 = maxEdges * 2; + int vertexDegree = vertex_degree; + int two = 2; + int i, j; + + int *tmp_arr; + + // Build and write EOE array + tmp_arr = new int[nEdges*maxEdges2]; + + for(i = 0; i < nEdges; i++){ + for(j = 0; j < maxEdges2; j++){ + tmp_arr[i*maxEdges2 + j] = 0; + } + } + + i = 0; + for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*maxEdges2 + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnEdge", NC_INT, "edges adj. to each edge", {"nEdges", "maxEdges2"}); ncutil::put_var(outputFilename, "edgesOnEdge", &tmp_arr[0]); - delete[] tmp_arr; - - // Build and write COE array - tmp_arr = new int[nEdges*two]; - for(i = 0; i < nEdges; i++){ - for(j = 0; j < two; j++){ - tmp_arr[i*two + j] = 0; - } - } - i = 0; - for(vec_int_itr = cellsOnEdge.begin(); vec_int_itr != cellsOnEdge.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*two + j] = (*int_itr) + 1; - j++; - } - - i++; - } - - ncutil::def_var(outputFilename, "cellsOnEdge", + delete[] tmp_arr; + + // Build and write COE array + tmp_arr = new int[nEdges*two]; + for(i = 0; i < nEdges; i++){ + for(j = 0; j < two; j++){ + tmp_arr[i*two + j] = 0; + } + } + i = 0; + for(vec_int_itr = cellsOnEdge.begin(); vec_int_itr != cellsOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*two + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "cellsOnEdge", NC_INT, "cells adj. to each edge", {"nEdges", "TWO"}); ncutil::put_var(outputFilename, "cellsOnEdge", &tmp_arr[0]); - // Build VOE array - i = 0; - for(vec_int_itr = verticesOnEdge.begin(); vec_int_itr != verticesOnEdge.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*two + j] = (*int_itr) + 1; - j++; - } + // Build VOE array + i = 0; + for(vec_int_itr = verticesOnEdge.begin(); vec_int_itr != verticesOnEdge.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*two + j] = (*int_itr) + 1; + j++; + } - i++; - } + i++; + } - ncutil::def_var(outputFilename, "verticesOnEdge", + ncutil::def_var(outputFilename, "verticesOnEdge", NC_INT, "vertices on each edge", {"nEdges", "TWO"}); ncutil::put_var(outputFilename, "verticesOnEdge", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - // Build and write nEoe array - tmp_arr = new int[nEdges]; - i = 0; - for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ - tmp_arr[i] = (*vec_int_itr).size(); - i++; - } + // Build and write nEoe array + tmp_arr = new int[nEdges]; + i = 0; + for(vec_int_itr = edgesOnEdge.begin(); vec_int_itr != edgesOnEdge.end(); ++vec_int_itr){ + tmp_arr[i] = (*vec_int_itr).size(); + i++; + } - ncutil::def_var(outputFilename, "nEdgesOnEdge", + ncutil::def_var(outputFilename, "nEdgesOnEdge", NC_INT, "number of edges on each edge", {"nEdges"}); ncutil::put_var(outputFilename, "nEdgesOnEdge", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - cellsOnEdge.clear(); -// verticesOnEdge.clear(); // Needed for Initial conditions. - edgesOnEdge.clear(); + cellsOnEdge.clear(); +// verticesOnEdge.clear(); // Needed for Initial conditions. + edgesOnEdge.clear(); - return 0; + return 0; }/*}}}*/ int outputVertexConnectivity( const string outputFilename) {/*{{{*/ - /***************************************************************** - * - * This function writes all of the *OnVertex arrays. Including - * cellsOnVertex - * edgesOnVertex - * - * ***************************************************************/ - - int nVertices = vertices.size(); - int vertexDegree = vertex_degree; - int i, j; - - int *tmp_arr; - - // Build and write COV array - tmp_arr = new int[nVertices*vertexDegree]; - - for(i = 0; i < nVertices; i++){ - for(j = 0; j < vertexDegree; j++){ - tmp_arr[i*vertexDegree + j] = 0; - } - } - - i = 0; - for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; - j++; - } - i++; - } - - ncutil::def_var(outputFilename, "cellsOnVertex", + /***************************************************************** + * + * This function writes all of the *OnVertex arrays. Including + * cellsOnVertex + * edgesOnVertex + * + * ***************************************************************/ + + int nVertices = vertices.size(); + int vertexDegree = vertex_degree; + int i, j; + + int *tmp_arr; + + // Build and write COV array + tmp_arr = new int[nVertices*vertexDegree]; + + for(i = 0; i < nVertices; i++){ + for(j = 0; j < vertexDegree; j++){ + tmp_arr[i*vertexDegree + j] = 0; + } + } + + i = 0; + for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "cellsOnVertex", NC_INT, "vertices adj. to each vertex", {"nVertices", "vertexDegree"}); ncutil::put_var(outputFilename, "cellsOnVertex", &tmp_arr[0]); - // Build and write EOV array - for(i = 0; i < nVertices; i++){ - for(j = 0; j < vertexDegree; j++){ - tmp_arr[i*vertexDegree + j] = 0; - } - } - i = 0; - for(vec_int_itr = edgesOnVertex.begin(); vec_int_itr != edgesOnVertex.end(); ++vec_int_itr){ - j = 0; - for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ - tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; - j++; - } - - i++; - } - - ncutil::def_var(outputFilename, "edgesOnVertex", + // Build and write EOV array + for(i = 0; i < nVertices; i++){ + for(j = 0; j < vertexDegree; j++){ + tmp_arr[i*vertexDegree + j] = 0; + } + } + i = 0; + for(vec_int_itr = edgesOnVertex.begin(); vec_int_itr != edgesOnVertex.end(); ++vec_int_itr){ + j = 0; + for(int_itr = (*vec_int_itr).begin(); int_itr != (*vec_int_itr).end(); ++int_itr){ + tmp_arr[i*vertexDegree + j] = (*int_itr) + 1; + j++; + } + + i++; + } + + ncutil::def_var(outputFilename, "edgesOnVertex", NC_INT, "edges adj. to each vertex", {"nVertices", "vertexDegree"}); ncutil::put_var(outputFilename, "edgesOnVertex", &tmp_arr[0]); - delete[] tmp_arr; - - // Build and write bdryVert array - tmp_arr = new int[nVertices]; - - i = 0; - for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ - if((*vec_int_itr).size() == vertexDegree){ - tmp_arr[i] = 0; - } else { - tmp_arr[i] = 1; - } - i++; - } - - ncutil::def_var(outputFilename, "boundaryVertex", + delete[] tmp_arr; + + // Build and write bdryVert array + tmp_arr = new int[nVertices]; + + i = 0; + for(vec_int_itr = cellsOnVertex.begin(); vec_int_itr != cellsOnVertex.end(); ++vec_int_itr){ + if((*vec_int_itr).size() == vertexDegree){ + tmp_arr[i] = 0; + } else { + tmp_arr[i] = 1; + } + i++; + } + + ncutil::def_var(outputFilename, "boundaryVertex", NC_INT, "non-zero for each vertex on mesh boundary", {"nVertices"}); ncutil::put_var(outputFilename, "boundaryVertex", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - cellsOnVertex.clear(); - edgesOnVertex.clear(); + cellsOnVertex.clear(); + edgesOnVertex.clear(); - return 0; + return 0; }/*}}}*/ int outputCellParameters( const string outputFilename) {/*{{{*/ - /********************************************************* - * - * This function writes all cell parameters, including - * areaCell - * - * *******************************************************/ - - int nCells = cells.size(); - int i, j; - - if(spherical){ - for(i = 0; i < nCells; i ++){ - areaCell[i] = areaCell[i] * sphereRadius * sphereRadius; - } - } - - ncutil::def_var(outputFilename, "areaCell", + /********************************************************* + * + * This function writes all cell parameters, including + * areaCell + * + * *******************************************************/ + + int nCells = cells.size(); + int i, j; + + if(spherical){ + for(i = 0; i < nCells; i ++){ + areaCell[i] = areaCell[i] * sphereRadius * sphereRadius; + } + } + + ncutil::def_var(outputFilename, "areaCell", NC_DOUBLE, "surface areas of cells", {"nCells"}); ncutil::put_var(outputFilename, "areaCell", &areaCell[0]); - areaCell.clear(); + areaCell.clear(); - return 0; + return 0; }/*}}}*/ int outputVertexParameters( const string outputFilename) {/*{{{*/ - /********************************************************* - * - * This function writes all vertex parameters, including - * areaTriangle - * kiteAreasOnVertex - * - * *******************************************************/ - - int nVertices = vertices.size(); - int vertexDegree = vertex_degree; - int i, j; - int count; - - double *tmp_arr; - - if(spherical){ - for(i = 0; i < nVertices; i++){ - areaTriangle[i] = areaTriangle[i] * sphereRadius * sphereRadius; - } - } - - ncutil::def_var(outputFilename, "areaTriangle", + /********************************************************* + * + * This function writes all vertex parameters, including + * areaTriangle + * kiteAreasOnVertex + * + * *******************************************************/ + + int nVertices = vertices.size(); + int vertexDegree = vertex_degree; + int i, j; + int count; + + double *tmp_arr; + + if(spherical){ + for(i = 0; i < nVertices; i++){ + areaTriangle[i] = areaTriangle[i] * sphereRadius * sphereRadius; + } + } + + ncutil::def_var(outputFilename, "areaTriangle", NC_DOUBLE, "surface areas of dual cells", {"nVertices"}); ncutil::put_var(outputFilename, "areaTriangle", &areaTriangle[0]); - // Build and write kiteAreasOnVertex - // TODO: Fix kite area for quads? - tmp_arr = new double[nVertices*vertexDegree]; - for(i = 0; i < nVertices*vertexDegree; i++){ - tmp_arr[i] = 0.0; - } - i = 0; - count = 0; - for(vec_dbl_itr = kiteAreasOnVertex.begin(); vec_dbl_itr != kiteAreasOnVertex.end(); ++vec_dbl_itr){ - j = 0; - for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ - if(!spherical){ - tmp_arr[i*vertexDegree + j] = (*dbl_itr); - } else { - tmp_arr[i*vertexDegree + j] = (*dbl_itr) * sphereRadius * sphereRadius; - } - j++; - count++; - } - i++; - } - - ncutil::def_var(outputFilename, "kiteAreasOnVertex", - NC_DOUBLE, + // Build and write kiteAreasOnVertex + // TODO: Fix kite area for quads? + tmp_arr = new double[nVertices*vertexDegree]; + for(i = 0; i < nVertices*vertexDegree; i++){ + tmp_arr[i] = 0.0; + } + i = 0; + count = 0; + for(vec_dbl_itr = kiteAreasOnVertex.begin(); vec_dbl_itr != kiteAreasOnVertex.end(); ++vec_dbl_itr){ + j = 0; + for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ + if(!spherical){ + tmp_arr[i*vertexDegree + j] = (*dbl_itr); + } else { + tmp_arr[i*vertexDegree + j] = (*dbl_itr) * sphereRadius * sphereRadius; + } + j++; + count++; + } + i++; + } + + ncutil::def_var(outputFilename, "kiteAreasOnVertex", + NC_DOUBLE, "surface areas of overlap between cells and dual cells", {"nVertices", "vertexDegree"}); ncutil::put_var(outputFilename, "kiteAreasOnVertex", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - kiteAreasOnVertex.clear(); + kiteAreasOnVertex.clear(); - return 0; + return 0; }/*}}}*/ int outputEdgeParameters( const string outputFilename) {/*{{{*/ - /********************************************************* - * - * This function writes all grid parameters, including - * angleEdge - * dcEdge - * dvEdge - * weightsOnEdge - * - * *******************************************************/ - - int nEdges = edges.size(); - int maxEdges2 = maxEdges * 2; - int i, j; - - double *tmp_arr; - - if(spherical){ - for(i = 0; i < nEdges; i++){ - dcEdge[i] = dcEdge[i] * sphereRadius; - dvEdge[i] = dvEdge[i] * sphereRadius; - } - } + /********************************************************* + * + * This function writes all grid parameters, including + * angleEdge + * dcEdge + * dvEdge + * weightsOnEdge + * + * *******************************************************/ + + int nEdges = edges.size(); + int maxEdges2 = maxEdges * 2; + int i, j; + + double *tmp_arr; + + if(spherical){ + for(i = 0; i < nEdges; i++){ + dcEdge[i] = dcEdge[i] * sphereRadius; + dvEdge[i] = dvEdge[i] * sphereRadius; + } + } ncutil::def_var(outputFilename, "angleEdge", NC_DOUBLE, "angle to edges", {"nEdges"}) ; @@ -3087,145 +3087,145 @@ int outputEdgeParameters( const string outputFilename) {/*{{{*/ NC_DOUBLE, "length of arc between vertices", {"nEdges"}); ncutil::put_var(outputFilename, "dcEdge", &dcEdge[0]) ; - ncutil::put_var(outputFilename, "dvEdge", &dvEdge[0]) ; - - //Build and write weightsOnEdge - tmp_arr = new double[nEdges*maxEdges2]; - - i = 0; - for(vec_dbl_itr = weightsOnEdge.begin(); vec_dbl_itr != weightsOnEdge.end(); ++vec_dbl_itr){ - for(j = 0; j < maxEdges2; j++){ - tmp_arr[i*maxEdges2 + j] = 0.0; - } - - j = 0; - for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ - tmp_arr[i*maxEdges2 + j] = (*dbl_itr); - j++; - } - i++; - } - - ncutil::def_var(outputFilename, "weightsOnEdge", + ncutil::put_var(outputFilename, "dvEdge", &dvEdge[0]) ; + + //Build and write weightsOnEdge + tmp_arr = new double[nEdges*maxEdges2]; + + i = 0; + for(vec_dbl_itr = weightsOnEdge.begin(); vec_dbl_itr != weightsOnEdge.end(); ++vec_dbl_itr){ + for(j = 0; j < maxEdges2; j++){ + tmp_arr[i*maxEdges2 + j] = 0.0; + } + + j = 0; + for(dbl_itr = (*vec_dbl_itr).begin(); dbl_itr != (*vec_dbl_itr).end(); ++dbl_itr){ + tmp_arr[i*maxEdges2 + j] = (*dbl_itr); + j++; + } + i++; + } + + ncutil::def_var(outputFilename, "weightsOnEdge", NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); ncutil::put_var(outputFilename, "weightsOnEdge", &tmp_arr[0]); - delete[] tmp_arr; + delete[] tmp_arr; - angleEdge.clear(); - dcEdge.clear(); - dvEdge.clear(); - weightsOnEdge.clear(); + angleEdge.clear(); + dcEdge.clear(); + dvEdge.clear(); + weightsOnEdge.clear(); - return 0; + return 0; }/*}}}*/ int outputMeshDensity( const string outputFilename) {/*{{{*/ - /*************************************************************************** - * - * This function writes the meshDensity variable. Read in from the file SaveDensity - * - * *************************************************************************/ - - ncutil::def_var(outputFilename, "meshDensity", + /*************************************************************************** + * + * This function writes the meshDensity variable. Read in from the file SaveDensity + * + * *************************************************************************/ + + ncutil::def_var(outputFilename, "meshDensity", NC_DOUBLE, "mesh density distribution", {"nCells"}); ncutil::put_var(outputFilename, "meshDensity", &meshDensity[0]); - return 0; + return 0; }/*}}}*/ int outputMeshQualities( const string outputFilename) {/*{{{*/ - /*************************************************************************** - * - * This function writes the mesh quality variables. - * - cellQuality - * - gridSpacing - * - triangleQuality - * - triangleAnalgeQuality - * - obtuseTriangle - * - * *************************************************************************/ - - ncutil::def_var(outputFilename, "cellQuality", + /*************************************************************************** + * + * This function writes the mesh quality variables. + * - cellQuality + * - gridSpacing + * - triangleQuality + * - triangleAnalgeQuality + * - obtuseTriangle + * + * *************************************************************************/ + + ncutil::def_var(outputFilename, "cellQuality", NC_DOUBLE, "quality of mesh cells", {"nCells"}); ncutil::put_var(outputFilename, "cellQuality", &cellQuality[0]); - ncutil::def_var(outputFilename, "gridSpacing", + ncutil::def_var(outputFilename, "gridSpacing", NC_DOUBLE, "grid spacing distribution", {"nCells"}); ncutil::put_var(outputFilename, "gridSpacing", &cellQuality[0]); - ncutil::def_var(outputFilename, "triangleQuality", + ncutil::def_var(outputFilename, "triangleQuality", NC_DOUBLE, "quality of mesh dual cells", {"nVertices"}); ncutil::put_var(outputFilename, "triangleQuality", &triangleQuality[0]); - ncutil::def_var(outputFilename, "triangleAngleQuality", + ncutil::def_var(outputFilename, "triangleAngleQuality", NC_DOUBLE, "quality of mesh dual cells", {"nVertices"}); - ncutil::put_var(outputFilename, + ncutil::put_var(outputFilename, "triangleAngleQuality", &triangleAngleQuality [0]); - ncutil::def_var(outputFilename, "obtuseTriangle", - NC_INT, + ncutil::def_var(outputFilename, "obtuseTriangle", + NC_INT, "non-zero for any dual cell containing obtuse angles", {"nVertices"}); ncutil::put_var(outputFilename, "obtuseTriangle", &obtuseTriangle[0]); - cellQuality.clear(); - gridSpacing.clear(); - triangleQuality.clear(); - triangleAngleQuality.clear(); - obtuseTriangle.clear(); + cellQuality.clear(); + gridSpacing.clear(); + triangleQuality.clear(); + triangleAngleQuality.clear(); + obtuseTriangle.clear(); - return 0; + return 0; }/*}}}*/ int writeGraphFile(const string outputFilename){/*{{{*/ - ofstream graph(outputFilename.c_str()); + ofstream graph(outputFilename.c_str()); - int edgeCount = 0; + int edgeCount = 0; - for (int iCell = 0; iCell < nCells; iCell++){ - for ( int j = 0; j < cellsOnCell.at(iCell).size(); j++){ - int coc = cellsOnCell.at(iCell).at(j); + for (int iCell = 0; iCell < nCells; iCell++){ + for ( int j = 0; j < cellsOnCell.at(iCell).size(); j++){ + int coc = cellsOnCell.at(iCell).at(j); - if ( coc >= 0 && coc < nCells ) { - edgeCount++; - } - } - } + if ( coc >= 0 && coc < nCells ) { + edgeCount++; + } + } + } - edgeCount = edgeCount / 2; - graph << cells.size() << " " << edgeCount << endl; + edgeCount = edgeCount / 2; + graph << cells.size() << " " << edgeCount << endl; - for(int i = 0; i < cellsOnCell.size(); i++){ - for(int j = 0; j < cellsOnCell.at(i).size(); j++){ - if ( cellsOnCell.at(i).at(j) >= 0 ) { - graph << cellsOnCell.at(i).at(j)+1 << " "; - } - } - graph << endl; - } + for(int i = 0; i < cellsOnCell.size(); i++){ + for(int j = 0; j < cellsOnCell.at(i).size(); j++){ + if ( cellsOnCell.at(i).at(j) >= 0 ) { + graph << cellsOnCell.at(i).at(j)+1 << " "; + } + } + graph << endl; + } - graph.close(); + graph.close(); - return 0; + return 0; }/*}}}*/ /*}}}*/ string gen_random(const int len) {/*{{{*/ - static const char alphanum[] = - "0123456789" -// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; + static const char alphanum[] = + "0123456789" +// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; - string rand_str = ""; + string rand_str = ""; - for (int i = 0; i < len; ++i) { - rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; - } + for (int i = 0; i < len; ++i) { + rand_str += alphanum[rand() % (sizeof(alphanum) - 1)]; + } - return rand_str; + return rand_str; }/*}}}*/ diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h b/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h index 46095cc9e..add28c81b 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/netcdf_utils.h @@ -17,7 +17,7 @@ # include # include - + # pragma once # ifndef __NCUTIL__ @@ -46,7 +46,7 @@ if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for write: " + std::to_string(_retv)); if ((_retv = nc_def_dim( @@ -54,13 +54,13 @@ { nc_close(_ncid) ; throw std::invalid_argument( - "Error putting dimension " + + "Error putting dimension " + _name + " :" + std::to_string(_retv)); } if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -71,17 +71,17 @@ ) { int _retv, _ncid, _dtag; - + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); if ((_retv = nc_inq_dimid(_ncid, _name.c_str(), &_dtag))) { nc_close(_ncid) ; throw std::invalid_argument( - "Error getting dimension " + + "Error getting dimension " + _name + " : " + std::to_string(_retv)); } @@ -89,13 +89,13 @@ { nc_close(_ncid) ; throw std::invalid_argument( - "Error getting dimension " + + "Error getting dimension " + _name + " : " + std::to_string(_retv)); } if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -119,10 +119,10 @@ if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); - if ((_retv = nc_get_att(_ncid, + if ((_retv = nc_get_att(_ncid, _vtag, _name.c_str (), (void *) _vals))) { nc_close(_ncid) ; @@ -133,7 +133,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -149,7 +149,7 @@ if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); if ((_retv = nc_inq_attlen( @@ -170,10 +170,10 @@ "Error getting attribute " + _name + ": " + std::to_string(_retv)); } - + if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -192,11 +192,11 @@ if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for write: " + std::to_string(_retv)); if ((_retv = nc_put_att( - _ncid, _vtag, + _ncid, _vtag, _name.c_str(), _type, +1, (void *) &_vals))) { nc_close(_ncid) ; @@ -207,7 +207,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -219,13 +219,13 @@ ) { int _retv, _ncid; - + if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); - if ((_retv = nc_put_att_text(_ncid, _vtag, + if ((_retv = nc_put_att_text(_ncid, _vtag, _name.c_str (), _vals.size(), _vals.c_str()))) { nc_close(_ncid) ; @@ -233,10 +233,10 @@ "Error getting attribute " + _name + ": " + std::to_string(_retv)); } - + if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -256,11 +256,11 @@ if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for write: " + std::to_string(_retv)); if ((_retv = nc_put_att( - _ncid, _vtag , + _ncid, _vtag , _name.c_str(), _type, _nval, (void *) _vals))) { nc_close(_ncid) ; @@ -271,7 +271,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -294,12 +294,12 @@ if (_dims.size() > +256) throw std::invalid_argument( - "Error putting variable " + + "Error putting variable " + _name + " : too many dimensions!"); if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for write: " + std::to_string(_retv)); for (auto _elem : _dims) @@ -309,13 +309,13 @@ { nc_close(_ncid) ; throw std::invalid_argument( - "Error putting variable " + + "Error putting variable " + _name + " : " + std::to_string(_retv)); } } if ((_retv = nc_def_var( - _ncid, _name.c_str(), + _ncid, _name.c_str(), _type, _dims.size (), _dtag, &_vtag))) { nc_close(_ncid) ; @@ -324,7 +324,7 @@ _name + ": " + std::to_string(_retv)); } - if ((_retv = nc_put_att_text(_ncid, _vtag, + if ((_retv = nc_put_att_text(_ncid, _vtag, "long_name", _long.size(), _long.c_str()))) { nc_close(_ncid) ; @@ -335,7 +335,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -352,14 +352,14 @@ if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); if ((_retv = nc_inq_varid(_ncid, _name.c_str(), &_vtag))) { nc_close(_ncid) ; throw std::invalid_argument( - "Error getting variable " + + "Error getting variable " + _name + " : " + std::to_string(_retv)); } @@ -373,7 +373,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -390,14 +390,14 @@ if ((_retv = nc_open(_file.c_str(), NC_WRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for write: " + std::to_string(_retv)); if ((_retv = nc_inq_varid(_ncid, _name.c_str(), &_vtag))) { nc_close(_ncid) ; throw std::invalid_argument( - "Error putting variable " + + "Error putting variable " + _name + " : " + std::to_string(_retv)); } @@ -411,7 +411,7 @@ if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } @@ -433,10 +433,10 @@ ) { int _retv, _ncid; - + if ((_retv = nc_open(_file.c_str(), NC_NOWRITE, &_ncid))) throw std::invalid_argument( - "Can't open " + _file + + "Can't open " + _file + " for read.: " + std::to_string(_retv)); if (_kind == NC_DIMENSION) @@ -449,7 +449,7 @@ "Error getting attribute " + _name + ": " + std::to_string(_retv)); } - } + } else if (_kind == NC_VARIABLE) { @@ -461,7 +461,7 @@ "Error getting attribute " + _name + ": " + std::to_string(_retv)); } - } + } else if (_kind == NC_ATTRIBUTE) { @@ -474,10 +474,10 @@ _name + ": " + std::to_string(_retv)); } } - + if ((_retv = nc_close(_ncid))) throw std::invalid_argument( - "Error handling " + + "Error handling " + _file + " close: " + std::to_string(_retv)); } diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h index 0c8100038..cd587bb6b 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h @@ -4,506 +4,506 @@ #include class pnt {/*{{{*/ - public: - double x, y, z; - double lat, lon; - int idx; - bool positiveLonRange; - - pnt(double x_, double y_, double z_, int idx_) { - (*this).x = x_; - (*this).y = y_; - (*this).z = z_; - (*this).idx = idx_; - (*this).positiveLonRange = true; - (*this).buildLat(); - (*this).buildLon(); - } - - pnt(double x_, double y_, double z_) { - (*this).x = x_; - (*this).y = y_; - (*this).z = z_; - (*this).idx = 0; - (*this).positiveLonRange = true; - (*this).buildLat(); - (*this).buildLon(); - } - - pnt() { - (*this).x = 0.0; - (*this).y = 0.0; - (*this).z = 0.0; - (*this).idx = 0; - (*this).positiveLonRange = true; - (*this).lat = 0.0; - (*this).lon = 0.0; - } - - friend pnt operator*(const double d, const pnt &p); - friend std::ostream & operator<<(std::ostream &os, const pnt &p); - friend std::istream & operator>>(std::istream &is, pnt &p); - - pnt& operator=(const pnt &p){/*{{{*/ - x = p.x; - y = p.y; - z = p.z; - idx = p.idx; - (*this).buildLat(); - (*this).buildLon(); - return *this; - }/*}}}*/ - bool operator==(const pnt &p) const {/*{{{*/ - return (x == p.x) & (y == p.y) & (z == p.z); - }/*}}}*/ - pnt operator-(const pnt &p) const {/*{{{*/ - double x_, y_, z_; - - x_ = x-p.x; - y_ = y-p.y; - z_ = z-p.z; - - return pnt(x_,y_,z_,0); - }/*}}}*/ - pnt operator+(const pnt &p) const {/*{{{*/ - double x_, y_, z_; - - x_ = x+p.x; - y_ = y+p.y; - z_ = z+p.z; - - return pnt(x_,y_,z_,0); - }/*}}}*/ - pnt operator*(double d) const {/*{{{*/ - double x_, y_, z_; - x_ = x*d; - y_ = y*d; - z_ = z*d; - return pnt(x_,y_,z_,0); - }/*}}}*/ - pnt operator/(double d) const {/*{{{*/ - double x_, y_, z_; - - if(d == 0.0){ - std::cout << "pnt: operator/" << std::endl; - std::cout << (*this) << std::endl; - } - - assert(d != 0.0); - x_ = x/d; - y_ = y/d; - z_ = z/d; - return pnt(x_,y_,z_,0); - }/*}}}*/ - pnt& operator/=(double d){/*{{{*/ - if(d == 0.0){ - std::cout << "pnt: operator /=" << std::endl << (*this) << std::endl; - } - assert(d != 0.0); - x = x/d; - y = y/d; - z = z/d; - return *this; - }/*}}}*/ - pnt& operator+=(const pnt &p){/*{{{*/ - x += p.x; - y += p.y; - z += p.z; - return *this; - }/*}}}*/ - double operator[](int i) const {/*{{{*/ - if(i == 0){ - return x; - } else if(i == 1){ - return y; - } else { - return z; - } - }/*}}}*/ - void normalize(){/*{{{*/ - double norm; - - norm = x*x + y*y + z*z; - if(norm == 0){ - std::cout << "pnt: normalize" << std::endl; - std::cout << x << " " << y << " " << z << " " << idx << std::endl; - - assert(norm != 0); - } - norm = sqrt(norm); - - x = x/norm; - y = y/norm; - z = z/norm; - }/*}}}*/ - double dot(const pnt &p) const {/*{{{*/ - double junk; - junk = x*p.x+y*p.y+z*p.z; - - return junk; - }/*}}}*/ - double dotForAngle(const pnt &p) const {/*{{{*/ - double junk; - junk = x*p.x+y*p.y+z*p.z; - if(junk > 1.0){ - junk = 1.0; - } - - if(junk < -1.0){ - junk = -1.0; - } - return acos(junk); - }/*}}}*/ - pnt cross(const pnt &p) const {/*{{{*/ - double x_, y_, z_; - - x_ = y*p.z - p.y*z; - y_ = z*p.x - p.z*x; - z_ = x*p.y - p.x*y; - - return pnt(x_,y_,z_,0); - }/*}}}*/ - void fixPeriodicity(const pnt &p, const double xRef, const double yRef){/*{{{*/ - /* The fixPeriodicity function fixes the periodicity of the current point relative - * to point p. It should only be used on a point in the x/y plane. - * xRef and yRef should be the extents of the non-periodic plane. - */ - - pnt dist_vec; - - dist_vec = (*this) - p; - - if(fabs(dist_vec.x) > xRef * 0.6){ + public: + double x, y, z; + double lat, lon; + int idx; + bool positiveLonRange; + + pnt(double x_, double y_, double z_, int idx_) { + (*this).x = x_; + (*this).y = y_; + (*this).z = z_; + (*this).idx = idx_; + (*this).positiveLonRange = true; + (*this).buildLat(); + (*this).buildLon(); + } + + pnt(double x_, double y_, double z_) { + (*this).x = x_; + (*this).y = y_; + (*this).z = z_; + (*this).idx = 0; + (*this).positiveLonRange = true; + (*this).buildLat(); + (*this).buildLon(); + } + + pnt() { + (*this).x = 0.0; + (*this).y = 0.0; + (*this).z = 0.0; + (*this).idx = 0; + (*this).positiveLonRange = true; + (*this).lat = 0.0; + (*this).lon = 0.0; + } + + friend pnt operator*(const double d, const pnt &p); + friend std::ostream & operator<<(std::ostream &os, const pnt &p); + friend std::istream & operator>>(std::istream &is, pnt &p); + + pnt& operator=(const pnt &p){/*{{{*/ + x = p.x; + y = p.y; + z = p.z; + idx = p.idx; + (*this).buildLat(); + (*this).buildLon(); + return *this; + }/*}}}*/ + bool operator==(const pnt &p) const {/*{{{*/ + return (x == p.x) & (y == p.y) & (z == p.z); + }/*}}}*/ + pnt operator-(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = x-p.x; + y_ = y-p.y; + z_ = z-p.z; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator+(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = x+p.x; + y_ = y+p.y; + z_ = z+p.z; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator*(double d) const {/*{{{*/ + double x_, y_, z_; + x_ = x*d; + y_ = y*d; + z_ = z*d; + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt operator/(double d) const {/*{{{*/ + double x_, y_, z_; + + if(d == 0.0){ + std::cout << "pnt: operator/" << std::endl; + std::cout << (*this) << std::endl; + } + + assert(d != 0.0); + x_ = x/d; + y_ = y/d; + z_ = z/d; + return pnt(x_,y_,z_,0); + }/*}}}*/ + pnt& operator/=(double d){/*{{{*/ + if(d == 0.0){ + std::cout << "pnt: operator /=" << std::endl << (*this) << std::endl; + } + assert(d != 0.0); + x = x/d; + y = y/d; + z = z/d; + return *this; + }/*}}}*/ + pnt& operator+=(const pnt &p){/*{{{*/ + x += p.x; + y += p.y; + z += p.z; + return *this; + }/*}}}*/ + double operator[](int i) const {/*{{{*/ + if(i == 0){ + return x; + } else if(i == 1){ + return y; + } else { + return z; + } + }/*}}}*/ + void normalize(){/*{{{*/ + double norm; + + norm = x*x + y*y + z*z; + if(norm == 0){ + std::cout << "pnt: normalize" << std::endl; + std::cout << x << " " << y << " " << z << " " << idx << std::endl; + + assert(norm != 0); + } + norm = sqrt(norm); + + x = x/norm; + y = y/norm; + z = z/norm; + }/*}}}*/ + double dot(const pnt &p) const {/*{{{*/ + double junk; + junk = x*p.x+y*p.y+z*p.z; + + return junk; + }/*}}}*/ + double dotForAngle(const pnt &p) const {/*{{{*/ + double junk; + junk = x*p.x+y*p.y+z*p.z; + if(junk > 1.0){ + junk = 1.0; + } + + if(junk < -1.0){ + junk = -1.0; + } + return acos(junk); + }/*}}}*/ + pnt cross(const pnt &p) const {/*{{{*/ + double x_, y_, z_; + + x_ = y*p.z - p.y*z; + y_ = z*p.x - p.z*x; + z_ = x*p.y - p.x*y; + + return pnt(x_,y_,z_,0); + }/*}}}*/ + void fixPeriodicity(const pnt &p, const double xRef, const double yRef){/*{{{*/ + /* The fixPeriodicity function fixes the periodicity of the current point relative + * to point p. It should only be used on a point in the x/y plane. + * xRef and yRef should be the extents of the non-periodic plane. + */ + + pnt dist_vec; + + dist_vec = (*this) - p; + + if(fabs(dist_vec.x) > xRef * 0.6){ #ifdef _DEBUG - std::cout << " Fixing x periodicity " << endl; + std::cout << " Fixing x periodicity " << endl; #endif - (*this).x += -(dist_vec.x/fabs(dist_vec.x)) * xRef; - } - if(fabs(dist_vec.y) > yRef * 0.6){ + (*this).x += -(dist_vec.x/fabs(dist_vec.x)) * xRef; + } + if(fabs(dist_vec.y) > yRef * 0.6){ #ifdef _DEBUG - std::cout << " Fixing y periodicity " << endl; + std::cout << " Fixing y periodicity " << endl; #endif - (*this).y += -(dist_vec.y/fabs(dist_vec.y)) * yRef; - } - - }/*}}}*/ - double magnitude() const{/*{{{*/ - return sqrt(x*x + y*y + z*z); - }/*}}}*/ - double magnitude2() const {/*{{{*/ - return x*x + y*y + z*z; - }/*}}}*/ - void rotate(const pnt &vec, const double angle){/*{{{*/ - double x_, y_, z_; - double cos_t, sin_t; - - cos_t = cos(angle); - sin_t = sin(angle); - - x_ = (cos_t + vec.x*vec.x *(1.0-cos_t)) * x - +(vec.x*vec.y*(1.0-cos_t) - vec.z * sin_t) * y - +(vec.x*vec.z*(1.0-cos_t) + vec.y * sin_t) * z; - - y_ = (vec.y*vec.x*(1.0-cos_t) + vec.z*sin_t) * x - +(cos_t + vec.y*vec.x*(1.0 - cos_t)) * y - +(vec.y*vec.z*(1.0-cos_t) - vec.x*sin_t) * z; - - z_ = (vec.z*vec.x*(1.0-cos_t)-vec.y*sin_t)*x - +(vec.z*vec.y*(1.0-cos_t)-vec.x*sin_t)*y - +(cos_t + vec.x*vec.x*(1.0-cos_t))*z; - - x = x_; - y = y_; - z = z_; - }/*}}}*/ - void setPositiveLonRange(bool value){/*{{{*/ - (*this).positiveLonRange = value; - (*this).buildLon(); - }/*}}}*/ - double getLat() const {/*{{{*/ - return (*this).lat; - }/*}}}*/ - double getLon() const {/*{{{*/ - return (*this).lon; - }/*}}}*/ - void buildLat() {/*{{{*/ - double dl; - - dl = sqrt((*this).x*(*this).x + (*this).y*(*this).y + (*this).z*(*this).z); - (*this).lat = asin(z/dl); - }/*}}}*/ - void buildLon() {/*{{{*/ - double lon; - - lon = atan2(y, x); - - // If the prime meridian is the minimum, this means the degree - // range is 0-360, so we need to translate. - if ( (*this).positiveLonRange ) { - if(lon < 0.0) { - lon = 2.0*M_PI + lon; - } - } - - (*this).lon = lon; - }/*}}}*/ - double sphereDistance(const pnt &p) const {/*{{{*/ - double arg; - - arg = sqrt( powf(sin(0.5*((*this).getLat()-p.getLat())),2) + - cos(p.getLat())*cos((*this).getLat())*powf(sin(0.5*((*this).getLon()-p.getLon())),2)); - return 2.0*asin(arg); - - /* - pnt temp, cross; - double dot; - temp.x = x; - temp.y = y; - temp.z = z; - - cross = temp.cross(p); - dot = temp.dot(p); - - - - return atan2(cross.magnitude(),dot); - // */ - }/*}}}*/ - struct hasher {/*{{{*/ - size_t operator()(const pnt &p) const { - uint32_t hash; - size_t i, key[3] = { (size_t)p.x, (size_t)p.y, (size_t)p.z }; - for(hash = i = 0; i < sizeof(key); ++i) { - hash += ((uint8_t *)key)[i]; - hash += (hash << 10); - hash ^= (hash >> 6); - } - hash += (hash << 3); - hash ^= (hash >> 11); - hash += (hash << 15); - return hash; - } - };/*}}}*/ - struct idx_hasher {/*{{{*/ - size_t operator()(const pnt &p) const { - return (size_t)p.idx; - } - };/*}}}*/ + (*this).y += -(dist_vec.y/fabs(dist_vec.y)) * yRef; + } + + }/*}}}*/ + double magnitude() const{/*{{{*/ + return sqrt(x*x + y*y + z*z); + }/*}}}*/ + double magnitude2() const {/*{{{*/ + return x*x + y*y + z*z; + }/*}}}*/ + void rotate(const pnt &vec, const double angle){/*{{{*/ + double x_, y_, z_; + double cos_t, sin_t; + + cos_t = cos(angle); + sin_t = sin(angle); + + x_ = (cos_t + vec.x*vec.x *(1.0-cos_t)) * x + +(vec.x*vec.y*(1.0-cos_t) - vec.z * sin_t) * y + +(vec.x*vec.z*(1.0-cos_t) + vec.y * sin_t) * z; + + y_ = (vec.y*vec.x*(1.0-cos_t) + vec.z*sin_t) * x + +(cos_t + vec.y*vec.x*(1.0 - cos_t)) * y + +(vec.y*vec.z*(1.0-cos_t) - vec.x*sin_t) * z; + + z_ = (vec.z*vec.x*(1.0-cos_t)-vec.y*sin_t)*x + +(vec.z*vec.y*(1.0-cos_t)-vec.x*sin_t)*y + +(cos_t + vec.x*vec.x*(1.0-cos_t))*z; + + x = x_; + y = y_; + z = z_; + }/*}}}*/ + void setPositiveLonRange(bool value){/*{{{*/ + (*this).positiveLonRange = value; + (*this).buildLon(); + }/*}}}*/ + double getLat() const {/*{{{*/ + return (*this).lat; + }/*}}}*/ + double getLon() const {/*{{{*/ + return (*this).lon; + }/*}}}*/ + void buildLat() {/*{{{*/ + double dl; + + dl = sqrt((*this).x*(*this).x + (*this).y*(*this).y + (*this).z*(*this).z); + (*this).lat = asin(z/dl); + }/*}}}*/ + void buildLon() {/*{{{*/ + double lon; + + lon = atan2(y, x); + + // If the prime meridian is the minimum, this means the degree + // range is 0-360, so we need to translate. + if ( (*this).positiveLonRange ) { + if(lon < 0.0) { + lon = 2.0*M_PI + lon; + } + } + + (*this).lon = lon; + }/*}}}*/ + double sphereDistance(const pnt &p) const {/*{{{*/ + double arg; + + arg = sqrt( powf(sin(0.5*((*this).getLat()-p.getLat())),2) + + cos(p.getLat())*cos((*this).getLat())*powf(sin(0.5*((*this).getLon()-p.getLon())),2)); + return 2.0*asin(arg); + + /* + pnt temp, cross; + double dot; + temp.x = x; + temp.y = y; + temp.z = z; + + cross = temp.cross(p); + dot = temp.dot(p); + + + + return atan2(cross.magnitude(),dot); + // */ + }/*}}}*/ + struct hasher {/*{{{*/ + size_t operator()(const pnt &p) const { + uint32_t hash; + size_t i, key[3] = { (size_t)p.x, (size_t)p.y, (size_t)p.z }; + for(hash = i = 0; i < sizeof(key); ++i) { + hash += ((uint8_t *)key)[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + };/*}}}*/ + struct idx_hasher {/*{{{*/ + size_t operator()(const pnt &p) const { + return (size_t)p.idx; + } + };/*}}}*/ };/*}}}*/ inline pnt operator*(const double d, const pnt &p){/*{{{*/ - return pnt(d*p.x, d*p.y, d*p.z, 0); + return pnt(d*p.x, d*p.y, d*p.z, 0); }/*}}}*/ inline std::ostream & operator<<(std::ostream &os, const pnt &p){/*{{{*/ - os << '(' << p.x << ", " << p.y << ", " << p.z << ") idx=" << p.idx << ' '; - //os << p.x << " " << p.y << " " << p.z; - return os; + os << '(' << p.x << ", " << p.y << ", " << p.z << ") idx=" << p.idx << ' '; + //os << p.x << " " << p.y << " " << p.z; + return os; }/*}}}*/ inline std::istream & operator>>(std::istream &is, pnt &p){/*{{{*/ - //is >> p.x >> p.y >> p.z >> p.idx; - is >> p.x >> p.y >> p.z; - return is; + //is >> p.x >> p.y >> p.z >> p.idx; + is >> p.x >> p.y >> p.z; + return is; }/*}}}*/ /* Geometric utility functions {{{ */ pnt gcIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){/*{{{*/ - /* - * gcIntersect is intended to compute the intersection of two great circles. - * The great circles pass through the point sets c1-c2 and v1-v2. - */ + /* + * gcIntersect is intended to compute the intersection of two great circles. + * The great circles pass through the point sets c1-c2 and v1-v2. + */ - /* - pnt n, m,c ; - double dot; + /* + pnt n, m,c ; + double dot; - //n = c1 cross c2 - n = c1.cross(c2); + //n = c1 cross c2 + n = c1.cross(c2); - //m = v1 cross v2 - m = v1.cross(v2); + //m = v1 cross v2 + m = v1.cross(v2); - //c = n cross m - c = n.cross(m); + //c = n cross m + c = n.cross(m); - dot = c1.dot(c); + dot = c1.dot(c); - dot = dot/fabs(dot); + dot = dot/fabs(dot); - c = c*dot; + c = c*dot; - c.normalize(); + c.normalize(); - return c; - // */ + return c; + // */ - double n1, n2, n3; - double m1, m2, m3; - double xc, yc, zc; - double dot; - pnt c; + double n1, n2, n3; + double m1, m2, m3; + double xc, yc, zc; + double dot; + pnt c; - n1 = (c1.y * c2.z - c2.y * c1.z); - n2 = -(c1.x * c2.z - c2.x * c1.z); - n3 = (c1.x * c2.y - c2.x * c1.y); + n1 = (c1.y * c2.z - c2.y * c1.z); + n2 = -(c1.x * c2.z - c2.x * c1.z); + n3 = (c1.x * c2.y - c2.x * c1.y); - m1 = (v1.y * v2.z - v2.y * v1.z); - m2 = -(v1.x * v2.z - v2.x * v1.z); - m3 = (v1.x * v2.y - v2.x * v1.y); + m1 = (v1.y * v2.z - v2.y * v1.z); + m2 = -(v1.x * v2.z - v2.x * v1.z); + m3 = (v1.x * v2.y - v2.x * v1.y); - xc = (n2 * m3 - n3 * m2); - yc = -(n1 * m3 - n3 * m1); - zc = (n1 * m2 - n2 * m1); + xc = (n2 * m3 - n3 * m2); + yc = -(n1 * m3 - n3 * m1); + zc = (n1 * m2 - n2 * m1); - dot = c1.x*xc + c1.y*yc + c1.z*zc; + dot = c1.x*xc + c1.y*yc + c1.z*zc; - if (dot < 0.0) { - xc = -xc; - yc = -yc; - zc = -zc; - } + if (dot < 0.0) { + xc = -xc; + yc = -yc; + zc = -zc; + } - c.x = xc; - c.y = yc; - c.z = zc; + c.x = xc; + c.y = yc; + c.z = zc; - c.normalize(); - return c; + c.normalize(); + return c; }/*}}}*/ pnt planarIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){/*{{{*/ - /* - * planarIntersect is intended to compute the point of intersection - * of the lines c1-c2 and v1-v2 in a plane. - */ - double alpha_numerator, beta_numerator, denom; - double alpha, beta; - pnt c; + /* + * planarIntersect is intended to compute the point of intersection + * of the lines c1-c2 and v1-v2 in a plane. + */ + double alpha_numerator, beta_numerator, denom; + double alpha, beta; + pnt c; - alpha_numerator = (v2.x - v1.x) * (c1.y - v2.y) - (v2.y - v1.y) * (c1.x - v2.x); -// beta_numerator = (c1.x - v2.x) * (c2.y - c1.y) - (c1.y - v2.y) * (c2.x - c1.x); - denom = (c2.x - c1.x) * (v2.y - v1.y) - (c2.y - c1.y) * (v2.x - v1.x); + alpha_numerator = (v2.x - v1.x) * (c1.y - v2.y) - (v2.y - v1.y) * (c1.x - v2.x); +// beta_numerator = (c1.x - v2.x) * (c2.y - c1.y) - (c1.y - v2.y) * (c2.x - c1.x); + denom = (c2.x - c1.x) * (v2.y - v1.y) - (c2.y - c1.y) * (v2.x - v1.x); - alpha = alpha_numerator / denom; -// beta = beta_numerator / demon; + alpha = alpha_numerator / denom; +// beta = beta_numerator / demon; - c = alpha * (v2 - v1) + v1; + c = alpha * (v2 - v1) + v1; - return c; + return c; }/*}}}*/ double planarTriangleArea(const pnt &A, const pnt &B, const pnt &C){/*{{{*/ - /* - * planarTriangleArea uses Heron's formula to compute the area of a triangle in a plane. - */ - pnt ab, bc, ca; - double s, a, b, c, e; + /* + * planarTriangleArea uses Heron's formula to compute the area of a triangle in a plane. + */ + pnt ab, bc, ca; + double s, a, b, c, e; - ab = B - A; - bc = C - B; - ca = A - C; + ab = B - A; + bc = C - B; + ca = A - C; - // Get side lengths - a = ab.magnitude(); - b = bc.magnitude(); - c = ca.magnitude(); + // Get side lengths + a = ab.magnitude(); + b = bc.magnitude(); + c = ca.magnitude(); - // Semi-perimeter of TRI(ABC) - s = (a + b + c) * 0.5; + // Semi-perimeter of TRI(ABC) + s = (a + b + c) * 0.5; - return sqrt(s * (s - a) * (s - b) * (s - c)); + return sqrt(s * (s - a) * (s - b) * (s - c)); }/*}}}*/ double sphericalTriangleArea(const pnt &A, const pnt &B, const pnt &C){/*{{{*/ - /* - * sphericalTriangleArea uses the spherical analog of Heron's formula to compute - * the area of a triangle on the surface of a sphere. - * - */ - double tanqe, s, a, b, c, e; - - a = A.sphereDistance(B); - b = B.sphereDistance(C); - c = C.sphereDistance(A); - s = 0.5*(a+b+c); - - tanqe = sqrt(tan(0.5*s)*tan(0.5*(s-a))*tan(0.5*(s-b))*tan(0.5*(s-c))); - e = 4.*atan(tanqe); - - return e; + /* + * sphericalTriangleArea uses the spherical analog of Heron's formula to compute + * the area of a triangle on the surface of a sphere. + * + */ + double tanqe, s, a, b, c, e; + + a = A.sphereDistance(B); + b = B.sphereDistance(C); + c = C.sphereDistance(A); + s = 0.5*(a+b+c); + + tanqe = sqrt(tan(0.5*s)*tan(0.5*(s-a))*tan(0.5*(s-b))*tan(0.5*(s-c))); + e = 4.*atan(tanqe); + + return e; }/*}}}*/ double planeAngle(const pnt &A, const pnt &B, const pnt &C, const pnt &n){/*{{{*/ - /* - * planeAngle computes the angles between the vectors AB and AC in a plane defined with - * the normal vector n - */ - - /* - pnt ab; - pnt ac; - pnt cross; - double cos_angle; - - ab = B - A; - ac = C - A; - cross = ab.cross(ac); - cos_angle = ab.dot(ac)/(ab.magnitude() * ac.magnitude()); - - cos_angle = std::min(std::max(cos_angle,-1.0),1.0); - - if(cross.x*n.x + cross.y*n.y + cross.z*n.z >= 0){ - return acos(cos_angle); - } else { - return -acos(cos_angle); - } - // */ - - double ABx, ABy, ABz, mAB; - double ACx, ACy, ACz, mAC; - double Dx, Dy, Dz; - double cos_angle; - - ABx = B.x - A.x; - ABy = B.y - A.y; - ABz = B.z - A.z; - mAB = sqrt(ABx*ABx + ABy*ABy + ABz*ABz); - - ACx = C.x - A.x; - ACy = C.y - A.y; - ACz = C.z - A.z; - mAC = sqrt(ACx*ACx + ACy*ACy + ACz*ACz); - - - Dx = (ABy * ACz) - (ABz * ACy); - Dy = -((ABx * ACz) - (ABz * ACx)); - Dz = (ABx * ACy) - (ABy * ACx); - - cos_angle = (ABx*ACx + ABy*ACy + ABz*ACz) / (mAB * mAC); - - if (cos_angle < -1.0) { - cos_angle = -1.0; - } else if (cos_angle > 1.0) { - cos_angle = 1.0; - } - - if ((Dx*n.x + Dy*n.y + Dz*n.z) >= 0.0) { - return acos(cos_angle); - } else { - return -acos(cos_angle); - } + /* + * planeAngle computes the angles between the vectors AB and AC in a plane defined with + * the normal vector n + */ + + /* + pnt ab; + pnt ac; + pnt cross; + double cos_angle; + + ab = B - A; + ac = C - A; + cross = ab.cross(ac); + cos_angle = ab.dot(ac)/(ab.magnitude() * ac.magnitude()); + + cos_angle = std::min(std::max(cos_angle,-1.0),1.0); + + if(cross.x*n.x + cross.y*n.y + cross.z*n.z >= 0){ + return acos(cos_angle); + } else { + return -acos(cos_angle); + } + // */ + + double ABx, ABy, ABz, mAB; + double ACx, ACy, ACz, mAC; + double Dx, Dy, Dz; + double cos_angle; + + ABx = B.x - A.x; + ABy = B.y - A.y; + ABz = B.z - A.z; + mAB = sqrt(ABx*ABx + ABy*ABy + ABz*ABz); + + ACx = C.x - A.x; + ACy = C.y - A.y; + ACz = C.z - A.z; + mAC = sqrt(ACx*ACx + ACy*ACy + ACz*ACz); + + + Dx = (ABy * ACz) - (ABz * ACy); + Dy = -((ABx * ACz) - (ABz * ACx)); + Dz = (ABx * ACy) - (ABy * ACx); + + cos_angle = (ABx*ACx + ABy*ACy + ABz*ACz) / (mAB * mAC); + + if (cos_angle < -1.0) { + cos_angle = -1.0; + } else if (cos_angle > 1.0) { + cos_angle = 1.0; + } + + if ((Dx*n.x + Dy*n.y + Dz*n.z) >= 0.0) { + return acos(cos_angle); + } else { + return -acos(cos_angle); + } }/*}}}*/ pnt pntFromLatLon(const double &lat, const double &lon){/*{{{*/ - /* - * pntFromLatLon constructs a point location in 3-Space - * from an initial latitude and longitude location. - */ - pnt temp; - temp.x = cos(lon) * cos(lat); - temp.y = sin(lon) * cos(lat); - temp.z = sin(lat); - temp.normalize(); - temp.buildLat(); - temp.buildLon(); - return temp; + /* + * pntFromLatLon constructs a point location in 3-Space + * from an initial latitude and longitude location. + */ + pnt temp; + temp.x = cos(lon) * cos(lat); + temp.y = sin(lon) * cos(lat); + temp.z = sin(lat); + temp.normalize(); + temp.buildLat(); + temp.buildLon(); + return temp; }/*}}}*/ /*}}}*/ From 08f9940afd77737642e5b1dc6f96677b9fc5abb2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 15:31:28 -0600 Subject: [PATCH 014/169] Add a `LoggingContext` to `check_call` We want to be able to call `mpas_tools.io.logging.check_call()` with `logger=None` so we can handle cases with and without a logger without any special treatment. --- conda_package/mpas_tools/logging.py | 45 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/conda_package/mpas_tools/logging.py b/conda_package/mpas_tools/logging.py index ca77f2bd8..8b1bfbbc0 100644 --- a/conda_package/mpas_tools/logging.py +++ b/conda_package/mpas_tools/logging.py @@ -3,7 +3,7 @@ import subprocess -def check_call(args, logger, log_command=True, timeout=None, **kwargs): +def check_call(args, logger=None, log_command=True, timeout=None, **kwargs): """ Call the given subprocess with logging to the given logger. @@ -13,7 +13,7 @@ def check_call(args, logger, log_command=True, timeout=None, **kwargs): A list or string of argument to the subprocess. If ``args`` is a string, you must pass ``shell=True`` as one of the ``kwargs``. - logger : logging.Logger + logger : logging.Logger, optional The logger to write output to log_command : bool, optional @@ -36,25 +36,28 @@ def check_call(args, logger, log_command=True, timeout=None, **kwargs): print_args = args else: print_args = ' '.join(args) - if log_command: - logger.info(f'Running: {print_args}') - - process = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, **kwargs) - stdout, stderr = process.communicate(timeout=timeout) - - if stdout: - stdout = stdout.decode('utf-8') - for line in stdout.split('\n'): - logger.info(line) - if stderr: - stderr = stderr.decode('utf-8') - for line in stderr.split('\n'): - logger.error(line) - - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, - print_args) + + # make a logger if there isn't already one + with LoggingContext(print_args, logger=logger) as logger: + if log_command: + logger.info(f'Running: {print_args}') + + process = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, **kwargs) + stdout, stderr = process.communicate(timeout=timeout) + + if stdout: + stdout = stdout.decode('utf-8') + for line in stdout.split('\n'): + logger.info(line) + if stderr: + stderr = stderr.decode('utf-8') + for line in stderr.split('\n'): + logger.error(line) + + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, + print_args) class LoggingContext(object): From 4c18afd4976988df6fc567c8d2801ec3c43714a7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:52:03 -0600 Subject: [PATCH 015/169] Add try/catch to cell culler The `cullCell` field doesn't necessarily exist and we want to not raise an exception if it doesn't. --- .../mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index 8185745c6..34c1c985b 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -396,7 +396,11 @@ int readGridInput(const string inputFilename){/*{{{*/ #endif cullCell.clear(); cullCell.resize(nCells); - ncutil::get_var(inputFilename, "cullCell", &cullCell[0]); + try { + ncutil::get_var(inputFilename, "cullCell", &cullCell[0]); + } catch (...) { + // allow errors for optional vars. not found + } // Build cellsOnVertex information cellsonvertex_list = new int[nVertices * vertexDegree]; From 285f667a6c7f6fc9c69a8bd817e91ceb9648d0e4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:53:37 -0600 Subject: [PATCH 016/169] Switch to using python mask creator in `mask()` For now, we don't have an alternative because the mask creator has not yet been rewritten to support NetCDF-C. This merge also switches all 3 functions in the mesh.creation module to use the logging.check_call() function, rather than an equivalent private function. --- conda_package/mpas_tools/mesh/conversion.py | 80 ++++++++------------- 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/conda_package/mpas_tools/mesh/conversion.py b/conda_package/mpas_tools/mesh/conversion.py index 629ffe424..d064d547e 100644 --- a/conda_package/mpas_tools/mesh/conversion.py +++ b/conda_package/mpas_tools/mesh/conversion.py @@ -1,10 +1,11 @@ import os import xarray -import subprocess from tempfile import TemporaryDirectory import shutil +import mpas_tools.io from mpas_tools.io import write_netcdf +from mpas_tools.logging import check_call def convert(dsIn, graphInfoFileName=None, logger=None, dir=None): @@ -48,9 +49,9 @@ def convert(dsIn, graphInfoFileName=None, logger=None, dir=None): outDir = os.path.dirname(outFileName) - _call_subprocess(['MpasMeshConverter.x', inFileName, outFileName], + check_call(['MpasMeshConverter.x', inFileName, outFileName], logger) - + dsOut = xarray.open_dataset(outFileName) dsOut.load() @@ -144,8 +145,8 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, outDir = os.path.dirname(outFileName) - _call_subprocess(args, logger) - + check_call(args=args, logger=logger) + dsOut = xarray.open_dataset(outFileName) dsOut.load() @@ -156,10 +157,10 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, return dsOut -def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None): +def mask(dsMesh, fcMask=None, logger=None, dir=None, cores=1): """ - Use ``MpasMaskCreator.x`` to create a set of region masks either from - mask feature collections or from seed points to be used to flood fill + Use ``compute_mpas_region_masks`` to create a set of region masks either + from mask feature collections Parameters ---------- @@ -169,10 +170,6 @@ def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None): fcMask : geometric_features.FeatureCollection, optional A feature collection containing features to use to create the mask - fcSeed : geometric_features.FeatureCollection, optional - A feature collection with points to use a seeds for a flood fill that - will create a mask of all cells connected to the seed points - logger : logging.Logger, optional A logger for the output if not stdout @@ -180,6 +177,9 @@ def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None): A directory in which a temporary directory will be added with files produced during mask creation and then deleted upon completion. + cores : int, optional + The number of cores to use for python multiprocessing + Returns ------- dsMask : xarray.Dataset @@ -189,48 +189,26 @@ def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None): if dir is not None: dir = os.path.abspath(dir) with TemporaryDirectory(dir=dir) as tempdir: - inFileName = '{}/mesh_in.nc'.format(tempdir) + inFileName = f'{tempdir}/mesh_in.nc' write_netcdf(dsMesh, inFileName) - outFileName = '{}/mesh_out.nc'.format(tempdir) - - args = ['MpasMaskCreator.x', inFileName, outFileName] - - if fcMask is not None: - fileName = '{}/mask.geojson'.format(tempdir) - fcMask.to_geojson(fileName) - args.extend(['-f', fileName]) - - if fcSeed is not None: - fileName = '{}/seed.geojson'.format(tempdir) - fcSeed.to_geojson(fileName) - args.extend(['-s', fileName]) - - _call_subprocess(args, logger) + outFileName = f'{tempdir}/mask_out.nc' + + geojsonFileName = f'{tempdir}/mask.geojson' + fcMask.to_geojson(geojsonFileName) + args = ['compute_mpas_region_masks', + '-m', inFileName, + '-o', outFileName, + '-g', geojsonFileName, + '-t', 'cell', + '--process_count', f'{cores}', + '--format', mpas_tools.io.default_format, + ] + if mpas_tools.io.default_engine is not None: + args.extend(['--engine', mpas_tools.io.default_engine]) + + check_call(args=args, logger=logger) dsOut = xarray.open_dataset(outFileName) dsOut.load() return dsOut - - -def _call_subprocess(args, logger): - """Call the given subprocess and send the output to the logger""" - if logger is None: - subprocess.check_call(args) - else: - process = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - if stdout: - stdout = stdout.decode('utf-8') - for line in stdout.split('\n'): - logger.info(line) - if stderr: - stderr = stderr.decode('utf-8') - for line in stderr.split('\n'): - logger.error(line) - - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, - ' '.join(args)) From 2a87bb291c812a6803046c5a4058c67234c5a6a1 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:55:50 -0600 Subject: [PATCH 017/169] Fix docstrings in mesh.mask --- conda_package/mpas_tools/mesh/mask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda_package/mpas_tools/mesh/mask.py b/conda_package/mpas_tools/mesh/mask.py index e4634d233..036bd5bb2 100644 --- a/conda_package/mpas_tools/mesh/mask.py +++ b/conda_package/mpas_tools/mesh/mask.py @@ -38,7 +38,7 @@ def compute_mpas_region_masks(dsMesh, fcMask, maskTypes=('cell', 'vertex'), maskTypes : tuple of {'cell', 'edge', 'vertex'}, optional Which type(s) of masks to make. Masks are created based on whether the latitude and longitude associated with each of these locations - (e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cells'``) are + (e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cell'``) are inside or outside of the regions in ``fcMask``. logger : logging.Logger, optional @@ -213,7 +213,7 @@ def compute_mpas_transect_masks(dsMesh, fcMask, earthRadius, maskTypes : tuple of {'cell', 'edge', 'vertex'}, optional Which type(s) of masks to make. Masks are created based on whether the latitude and longitude associated with each of these locations - (e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cells'``) are + (e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cell'``) are inside or outside of the transects in ``fcMask``. logger : logging.Logger, optional From 3640f88f3cf508968a925343b8ec0489f54b9363 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:56:08 -0600 Subject: [PATCH 018/169] Switch to subprocess calls in `build_mesh` The calls to `convert()` and `cull()` wrappers have caused trouble for big meshes. --- .../mpas_tools/mesh/creation/build_mesh.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/conda_package/mpas_tools/mesh/creation/build_mesh.py b/conda_package/mpas_tools/mesh/creation/build_mesh.py index 701624dc5..af3ddd40c 100644 --- a/conda_package/mpas_tools/mesh/creation/build_mesh.py +++ b/conda_package/mpas_tools/mesh/creation/build_mesh.py @@ -6,8 +6,7 @@ import cartopy.crs as ccrs import cartopy -from mpas_tools.mesh.conversion import convert -from mpas_tools.io import write_netcdf +from mpas_tools.logging import check_call from mpas_tools.mesh.creation.jigsaw_driver import jigsaw_driver from mpas_tools.mesh.creation.jigsaw_to_netcdf import jigsaw_to_netcdf @@ -101,9 +100,10 @@ def build_spherical_mesh(cellWidth, lon, lat, earth_radius, sphere_radius=earth_radius) logger.info('Step 3. Convert from triangles to MPAS mesh') - write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'), dir=dir, - logger=logger), - out_filename) + args = ['MpasMeshConverter.x', + 'mesh_triangles.nc', + out_filename] + check_call(args=args, logger=logger) def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges, @@ -152,7 +152,7 @@ def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges, output_name='mesh_triangles.nc', on_sphere=False) logger.info('Step 3. Convert from triangles to MPAS mesh') - write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'), - logger=logger), - out_filename) - + args = ['MpasMeshConverter.x', + 'mesh_triangles.nc', + out_filename] + check_call(args=args, logger=logger) From 91d3cb5314b2489dfde72beb80584bedd3367afa Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:57:22 -0600 Subject: [PATCH 019/169] Use subprocess call to `MpasCellCuller.x` in vtk extrator The `cull()` wrapper is more trouble in this case. --- conda_package/mpas_tools/viz/paraview_extractor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/conda_package/mpas_tools/viz/paraview_extractor.py b/conda_package/mpas_tools/viz/paraview_extractor.py index 6fa33a2ae..dfeaa434f 100644 --- a/conda_package/mpas_tools/viz/paraview_extractor.py +++ b/conda_package/mpas_tools/viz/paraview_extractor.py @@ -94,6 +94,7 @@ from progressbar import ProgressBar, Percentage, Bar, ETA from mpas_tools.conversion import mask, cull from mpas_tools.io import write_netcdf +from mpas_tools.logging import check_call def extract_vtk(filename_pattern, variable_list='all', dimension_list=None, @@ -2146,12 +2147,14 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names, print('Making a region mask file') ds_mask = mask(dsMesh=ds_mesh, fcMask=fc_region_mask, logger=logger, dir=temp_dir) - write_netcdf(ds_mask, '{}/mask.nc'.format(temp_dir)) + write_netcdf(ds_mask, f'{temp_dir}/mask.nc') print('Cropping mesh to region') out_mesh_filename = '{}/mesh.nc'.format(temp_dir) - ds_culled = cull(dsIn=ds_mesh, dsInverse=ds_mask, logger=logger, - dir=temp_dir) - write_netcdf(ds_culled, out_mesh_filename) + args = ['MpasCellCuller.x', + mesh_filename, + out_mesh_filename, + '-i', f'{temp_dir}/mask.nc'] + check_call(args=args, logger=logger) region_masks = dict() cell_mask = ds_mask.regionCellMasks.sum(dim='nRegions') > 0 @@ -2201,5 +2204,3 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names, handler.close() return out_mesh_filename, out_time_file_names - -# vim: set expandtab: From 325d225cefa95f3fae038b47ba0f5a9de01f2068 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 May 2023 21:59:41 -0600 Subject: [PATCH 020/169] Update recipe for NetCDF-C mesh conversion tools --- conda_package/recipe/build.sh | 2 +- conda_package/recipe/meta.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda_package/recipe/build.sh b/conda_package/recipe/build.sh index a8d25d4c5..870739f0e 100644 --- a/conda_package/recipe/build.sh +++ b/conda_package/recipe/build.sh @@ -20,7 +20,7 @@ cmake --build . cmake --install . # build and install mesh conversion tools -cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools +cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools_netcdf_c mkdir build cd build cmake \ diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index ab3bb74c5..8312dceb2 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -102,7 +102,7 @@ test: - translate_planar_grid -f 'periodic_mesh_10x20_1km.nc' -d 'periodic_mesh_20x40_1km.nc' - MpasMeshConverter.x mesh_tools/mesh_conversion_tools/test/mesh.QU.1920km.151026.nc mesh.nc - MpasCellCuller.x mesh.nc culled_mesh.nc -m mesh_tools/mesh_conversion_tools/test/land_mask_final.nc - - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson + # - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson - planar_hex --nx=30 --ny=20 --dc=1000. --npx --npy --outFileName='nonperiodic_mesh_30x20_1km.nc' - MpasCellCuller.x nonperiodic_mesh_30x20_1km.nc culled_nonperiodic_mesh_30x20_1km.nc - python -m pytest conda_package/mpas_tools/tests From 14dfecd3229906a0673c5c8bf20eadcb444bacc0 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Mon, 31 Jul 2023 08:25:03 -0700 Subject: [PATCH 021/169] Update SEACAS paths for Perlmutter and Chicoma Update SEACAS paths for Perlmutter and Chicoma, used by conversion_exodus_init_to_mpasli_mesh.py --- .../mesh_tools_li/conversion_exodus_init_to_mpasli_mesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/landice/mesh_tools_li/conversion_exodus_init_to_mpasli_mesh.py b/landice/mesh_tools_li/conversion_exodus_init_to_mpasli_mesh.py index 7d5123863..55c920707 100755 --- a/landice/mesh_tools_li/conversion_exodus_init_to_mpasli_mesh.py +++ b/landice/mesh_tools_li/conversion_exodus_init_to_mpasli_mesh.py @@ -30,7 +30,7 @@ parser.add_option("-a", "--ascii", dest="id_file", help="the ascii global id input file") parser.add_option("-o", "--out", dest="nc_file", help="the mpas file to write to") parser.add_option("-v", "--variable", dest="var_name", help="the mpas variable(s) you want to convert from an exodus file. May be a single variable or multiple variables comma-separated (no spaces)") -parser.add_option("--dynamics", dest="dynamics", action="store_true", help="Use to convert ice dynamics fields: beta, muFriction, stiffnessFactor, uReconstructX/Y. If stiffnessFactor was not included in the optimzation, it will be skipped.") +parser.add_option("--dynamics", dest="dynamics", action="store_true", help="Use to convert ice dynamics fields: beta, muFriction, stiffnessFactor, uReconstructX/Y. If stiffnessFactor was not included in the optimization, it will be skipped.") parser.add_option("--thermal", dest="thermal", action="store_true", help="Use to convert thermal fields: temperature, surfaceTemperature, basalTemperature. Only use when the Albany optimization included the thermal solution.") parser.add_option("--geometry", dest="geometry", action="store_true", help="Use to convert geometry fields: thickness and corresponding adjustment to bedTopography. Only use when the Albany optimization included adjustment to the ice thickness.") for option in parser.option_list: @@ -46,8 +46,8 @@ #sys.path.append('/home/tzhang/Apps/seacas/lib') #sys.path.append('/Users/trevorhillebrand/Documents/mpas/seacas/lib/') #sys.path.append('/Users/mhoffman/software/seacas/install/lib') - sys.path.append('/global/project/projectdirs/piscees/nightlyCoriCDash/build/TrilinosInstall/lib') # path on Cori - # Note: There is currently not an operational installation on Badger or Grizzly + sys.path.append('/global/common/software/fanssie/trilinos-serial-release-gnu/lib') # Perlmutter + #sys.path.append('/usr/projects/climate/SHARED_CLIMATE/software/badger/trilinos/2018-12-19/gcc-6.4.0/openmpi-2.1.2/lib') # Chicoma else: sys.path.append(SEACAS_path+'/lib') From 8e25380d54823eb8540d2428bd0c8a16189526a8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 13:46:20 +0200 Subject: [PATCH 022/169] Initialize cell seed mask to zero --- .../mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index 34c1c985b..17271da10 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -473,6 +473,10 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ cellSeedMask = new int[nCells]; flattenedMask = new int[nCells]; + for ( i = 0; i < nCells; i++){ + cellSeedMask[i] = 0; + } + try { ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); ncutil::get_var(masksFilename, "transectCellMasks", transectCellMasks); From 42cd3563efdc045c9ce7de06c5249a9a89929afd Mon Sep 17 00:00:00 2001 From: dengwirda Date: Fri, 4 Aug 2023 06:38:00 -0600 Subject: [PATCH 023/169] Clean-up mesh sorting --- .../mpas_tools/mesh/creation/sort_mesh.py | 186 +++++++++++------- conda_package/recipe/meta.yaml | 2 + 2 files changed, 122 insertions(+), 66 deletions(-) diff --git a/conda_package/mpas_tools/mesh/creation/sort_mesh.py b/conda_package/mpas_tools/mesh/creation/sort_mesh.py index f55375a5c..2eb75ad79 100644 --- a/conda_package/mpas_tools/mesh/creation/sort_mesh.py +++ b/conda_package/mpas_tools/mesh/creation/sort_mesh.py @@ -7,24 +7,20 @@ from scipy.sparse.csgraph import reverse_cuthill_mckee -def sort_fwd(data, fwd): - vals = data.values - vals = vals[fwd - 1] - return vals - - -def sort_rev(data, rev): - vals = data.values - mask = vals > 0 - vals[mask] = rev[vals[mask] - 1] - return vals - - def sort_mesh(mesh): """ - SORT-MESH: sort cells, edges and duals in the mesh - to improve cache-locality. - + Sort cells, edges and duals in the mesh + to improve cache-locality + + Parameters + ---------- + mesh : xarray.Dataset + A dataset containing an MPAS mesh to sort + + Returns + ------- + mesh : xarray.Dataset + A dataset containing the sorted MPAS mesh """ # Authors: Darren Engwirda @@ -41,22 +37,22 @@ def sort_mesh(mesh): # sort cells via RCM ordering of adjacency matrix - cell_fwd = reverse_cuthill_mckee(cell_del2(mesh)) + 1 + cell_fwd = reverse_cuthill_mckee(_cell_del2(mesh)) + 1 cell_rev = np.zeros(ncells, dtype=np.int32) cell_rev[cell_fwd - 1] = np.arange(ncells) + 1 mesh["cellsOnCell"][:] = \ - sort_rev(mesh["cellsOnCell"], cell_rev) + _sort_rev(mesh["cellsOnCell"], cell_rev) mesh["cellsOnEdge"][:] = \ - sort_rev(mesh["cellsOnEdge"], cell_rev) + _sort_rev(mesh["cellsOnEdge"], cell_rev) mesh["cellsOnVertex"][:] = \ - sort_rev(mesh["cellsOnVertex"], cell_rev) + _sort_rev(mesh["cellsOnVertex"], cell_rev) for var in mesh.keys(): dims = mesh.variables[var].dims if ("nCells" in dims): - mesh[var][:] = sort_fwd(mesh[var], cell_fwd) + mesh[var][:] = _sort_fwd(mesh[var], cell_fwd) mesh["indexToCellID"][:] = np.arange(ncells) + 1 @@ -73,15 +69,15 @@ def sort_mesh(mesh): dual_rev[dual_fwd - 1] = np.arange(nduals) + 1 mesh["verticesOnCell"][:] = \ - sort_rev(mesh["verticesOnCell"], dual_rev) + _sort_rev(mesh["verticesOnCell"], dual_rev) mesh["verticesOnEdge"][:] = \ - sort_rev(mesh["verticesOnEdge"], dual_rev) + _sort_rev(mesh["verticesOnEdge"], dual_rev) for var in mesh.keys(): dims = mesh.variables[var].dims if ("nVertices" in dims): - mesh[var][:] = sort_fwd(mesh[var], dual_fwd) + mesh[var][:] = _sort_fwd(mesh[var], dual_fwd) mesh["indexToVertexID"][:] = np.arange(nduals) + 1 @@ -98,29 +94,120 @@ def sort_mesh(mesh): edge_rev[edge_fwd - 1] = np.arange(nedges) + 1 mesh["edgesOnCell"][:] = \ - sort_rev(mesh["edgesOnCell"], edge_rev) + _sort_rev(mesh["edgesOnCell"], edge_rev) mesh["edgesOnEdge"][:] = \ - sort_rev(mesh["edgesOnEdge"], edge_rev) + _sort_rev(mesh["edgesOnEdge"], edge_rev) mesh["edgesOnVertex"][:] = \ - sort_rev(mesh["edgesOnVertex"], edge_rev) + _sort_rev(mesh["edgesOnVertex"], edge_rev) for var in mesh.keys(): dims = mesh.variables[var].dims if ("nEdges" in dims): - mesh[var][:] = sort_fwd(mesh[var], edge_fwd) + mesh[var][:] = _sort_fwd(mesh[var], edge_fwd) mesh["indexToEdgeID"][:] = np.arange(nedges) + 1 return mesh + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument( + "--mesh-file", dest="mesh_file", type=str, + required=True, help="Path+name to unsorted mesh file.") + + parser.add_argument( + "--sort-file", dest="sort_file", type=str, + required=True, help="Path+name to sorted output file.") + + args = parser.parse_args() + + mesh = xarray.open_dataset(args.mesh_file) + + sort_mesh(mesh) + + with open(os.path.join(os.path.dirname( + args.sort_file), "graph.info"), "w") as fptr: + cellsOnCell = mesh["cellsOnCell"].values + + ncells = mesh.dims["nCells"] + nedges = np.count_nonzero(cellsOnCell) // 2 + + fptr.write(f"{ncells} {nedges}\n") + for cell in range(ncells): + data = cellsOnCell[cell, :] + data = data[data > 0] + for item in data: + fptr.write(f"{item} ") + fptr.write("\n") + + mesh.to_netcdf(args.sort_file, format="NETCDF4") + + +def _sort_fwd(data, fwd): + """ + Apply a forward permutation to a mesh array + + Parameters + ---------- + data : array-like + An MPAS mesh array to permute + + fwd : numpy.ndarray + An array of integers defining the permutation + + Returns + ------- + data : numpy.ndarray + The forward permuted MPAS mesh array + """ + vals = data.values + vals = vals[fwd - 1] + return vals -def cell_del2(mesh): +def _sort_rev(data, rev): """ - CELL-DEL2: form cell-to-cell sparse adjacency graph + Apply a reverse permutation to a mesh array + + Parameters + ---------- + data : array-like + An MPAS mesh array to permute + + rev : numpy.ndarray + An array of integers defining the permutation + + Returns + ------- + data : numpy.ndarray + The reverse permuted MPAS mesh array + """ + vals = data.values + mask = vals > 0 + vals[mask] = rev[vals[mask] - 1] + return vals + +def _cell_del2(mesh): """ + Form cell-to-cell sparse adjacency graph + + Parameters + ---------- + mesh : xarray.Dataset + A dataset containing an MPAS mesh to sort + + Returns + ------- + del2 : scipy.sparse.csr_matrix + The cell-to-cell adjacency graph as a sparse matrix + """ xvec = np.array([], dtype=np.int8) ivec = np.array([], dtype=np.int32) jvec = np.array([], dtype=np.int32) @@ -149,39 +236,6 @@ def cell_del2(mesh): return csr_matrix((xvec, (ivec, jvec))) - -if (__name__ == "__main__"): - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawTextHelpFormatter) - - parser.add_argument( - "--mesh-file", dest="mesh_file", type=str, - required=True, help="Path+name to unsorted mesh file.") - - parser.add_argument( - "--sort-file", dest="sort_file", type=str, - required=True, help="Path+name to sorted output file.") - - args = parser.parse_args() - - mesh = xarray.open_dataset(args.mesh_file) - - sort_mesh(mesh) - - with open(os.path.join(os.path.dirname( - args.sort_file), "graph.info"), "w") as fptr: - cellsOnCell = mesh["cellsOnCell"].values - - ncells = mesh.dims["nCells"] - nedges = np.count_nonzero(cellsOnCell) // 2 - - fptr.write("{} {}\n".format(ncells, nedges)) - for cell in range(ncells): - data = cellsOnCell[cell, :] - data = data[data > 0] - for item in data: - fptr.write("{} ".format(item)) - fptr.write("\n") - - mesh.to_netcdf(args.sort_file, format="NETCDF4") + +if (__name__ == "__main__"):\ + main() diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index ab3bb74c5..188f16bea 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -33,6 +33,7 @@ build: - prepare_seaice_partitions = mpas_tools.seaice.partition:prepare_partitions - create_seaice_partitions = mpas_tools.seaice.partition:create_partitions - simple_seaice_partitions = mpas_tools.seaice.partition:simple_partitions + - sort_mesh = mpas_tools.mesh.creation.sort_mesh:main requirements: build: @@ -101,6 +102,7 @@ test: - planar_hex --nx=20 --ny=40 --dc=1000. --outFileName='periodic_mesh_20x40_1km.nc' - translate_planar_grid -f 'periodic_mesh_10x20_1km.nc' -d 'periodic_mesh_20x40_1km.nc' - MpasMeshConverter.x mesh_tools/mesh_conversion_tools/test/mesh.QU.1920km.151026.nc mesh.nc + - sort_mesh --mesh-file mesh.nc --sort-file sorted_mesh.nc - MpasCellCuller.x mesh.nc culled_mesh.nc -m mesh_tools/mesh_conversion_tools/test/land_mask_final.nc - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson - planar_hex --nx=30 --ny=20 --dc=1000. --npx --npy --outFileName='nonperiodic_mesh_30x20_1km.nc' From 1c79f77d5abc0e7354aed589b39224d297bb4cb8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 14:28:55 +0200 Subject: [PATCH 024/169] Fix try/catch clauses We need one per optional variable or attribute. Otherwise all subsequent variables or attributes get skipped when the first is not present. --- .../mpas_cell_culler.cpp | 44 +++++++++++++++++++ .../mpas_mesh_converter.cpp | 28 ++++++++++++ 2 files changed, 72 insertions(+) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index 17271da10..fd87bf098 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -333,35 +333,67 @@ int readGridInput(const string inputFilename){/*{{{*/ #endif ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); spherical = (on_a_sphere.find("YES") != string::npos); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading sphere_radius" << endl; #endif ncutil::get_att(inputFilename, "sphere_radius", &sphere_radius); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading history" << endl; #endif ncutil::get_str(inputFilename, "history", in_history); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading file_id" << endl; #endif ncutil::get_str(inputFilename, "file_id", in_file_id); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading parent_id" << endl; #endif ncutil::get_str(inputFilename, "parent_id", in_parent_id); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading parent_id" << endl; #endif ncutil::get_str(inputFilename, "mesh_spec", in_mesh_spec); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading is_periodic" << endl; #endif ncutil::get_str(inputFilename, "is_periodic", is_periodic); periodic = (is_periodic.find("YES") != string::npos); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading x_period" << endl; #endif ncutil::get_att(inputFilename, "x_period", &xPeriod); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading y_period" << endl; #endif @@ -463,6 +495,10 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ try { ncutil::get_dim(masksFilename, "nRegions", nRegions); + } catch (...) { + // allow errors for optional attr. not found + } + try { ncutil::get_dim(masksFilename, "nTransects", nTransects); } catch (...) { // allow errors for optional attr. not found @@ -479,7 +515,15 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ try { ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); + } catch (...) { + // allow errors for optional vars. not found + } + try { ncutil::get_var(masksFilename, "transectCellMasks", transectCellMasks); + } catch (...) { + // allow errors for optional vars. not found + } + try { ncutil::get_var(masksFilename, "cellSeedMask", cellSeedMask); } catch (...) { // allow errors for optional vars. not found diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp index 7a590eac4..159ef7416 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_mesh_converter.cpp @@ -325,31 +325,59 @@ int readGridInput(const string inputFilename){/*{{{*/ #endif ncutil::get_str(inputFilename, "on_a_sphere", on_a_sphere); spherical = (on_a_sphere.find("YES") != string::npos); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading sphere_radius" << endl; #endif ncutil::get_att(inputFilename, "sphere_radius", &sphereRadius); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading history" << endl; #endif ncutil::get_str(inputFilename, "history", in_history); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading file_id" << endl; #endif ncutil::get_str(inputFilename, "file_id", in_file_id); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading parent_id" << endl; #endif ncutil::get_str(inputFilename, "parent_id", in_parent_id); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading is_periodic" << endl; #endif ncutil::get_str(inputFilename, "is_periodic", is_periodic); periodic = (is_periodic.find("YES") != string::npos); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading x_period" << endl; #endif ncutil::get_att(inputFilename, "x_period", &xPeriod); + } catch (...) { + // allow errors for optional attr. not found + } + try { #ifdef _DEBUG cout << " Reading y_period" << endl; #endif From 64075b84f3d59276b4858e2ddc2c4b73eaaf6dc7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 14:39:12 +0200 Subject: [PATCH 025/169] Fix handling of cell culler -c flag Remove limit on number of arguments (since the code handles unlimited numbers of masks) --- .../mpas_cell_culler.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index fd87bf098..f780c4edb 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -142,13 +142,6 @@ int main ( int argc, char *argv[] ) { in_name = argv[1]; out_name = argv[2]; } - else if (argc >= 10) - { - cout << "\n"; - cout << " ERROR: Incorrect number of arguments specified. See usage statement" << endl; - print_usage(); - exit(1); - } else { cullMasks = true; @@ -156,7 +149,8 @@ int main ( int argc, char *argv[] ) { out_name = argv[2]; bool foundOperation; - for ( int i = 3; i < argc; i+=2 ) { + int i = 3; + while ( i < argc ) { foundOperation = false; if (strcmp(argv[i], "-m") == 0 ) { mask_ops.push_back(static_cast(mergeOp)); @@ -174,9 +168,11 @@ int main ( int argc, char *argv[] ) { print_usage(); exit(1); } + i++; if (foundOperation) { - mask_names.push_back( argv[i+1] ); + mask_names.push_back( argv[i] ); + i++; } } } From 3e28c2275bf0271cddba4b7545bdaf58d3fb4790 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 13:46:42 +0200 Subject: [PATCH 026/169] Print mask operation details in cell culler --- .../mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index f780c4edb..c286d6421 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -500,6 +500,12 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ // allow errors for optional attr. not found } + cout << "mask filename: " << masksFilename << endl; + cout << " nRegions: " << nRegions << endl; + cout << " nTransects: " << nTransects << endl; + cout << " maskOp: " << maskOp << endl; + cout << " nCells: " << nCells << endl; + regionCellMasks = new int[nCells*nRegions]; transectCellMasks = new int[nCells*nTransects]; cellSeedMask = new int[nCells]; From a343a37825df7e546c68fe0e871204e223e5fcf2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 17:45:22 +0200 Subject: [PATCH 027/169] Add some more output for debugging --- .../mpas_cell_culler.cpp | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index c286d6421..e4168333a 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -514,23 +514,22 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ for ( i = 0; i < nCells; i++){ cellSeedMask[i] = 0; } - try { - ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); + ncutil::get_var(masksFilename, "cellSeedMask", cellSeedMask); } catch (...) { // allow errors for optional vars. not found } - try { - ncutil::get_var(masksFilename, "transectCellMasks", transectCellMasks); - } catch (...) { - // allow errors for optional vars. not found + + if (nRegions > 0) { + cout << " Reading regionCellMasks" << endl; + ncutil::get_var(masksFilename, "regionCellMasks", regionCellMasks); } - try { - ncutil::get_var(masksFilename, "cellSeedMask", cellSeedMask); - } catch (...) { - // allow errors for optional vars. not found + if (nTransects > 0) { + cout << " Reading transectCellMasks" << endl; + ncutil::get_var(masksFilename, "transectCellMasks", transectCellMasks); } + cout << " Flattening seed, region and/or transect masks" << endl; for ( i = 0; i < nCells; i++){ flattenedMask[i] = cellSeedMask[i]; for ( j = 0; j < nRegions; j++){ @@ -542,17 +541,22 @@ int mergeCellMasks(const string masksFilename, const int maskOp){/*{{{*/ } } + cout << " Applying flattened mask to cullCell" << endl; + if ( maskOp == invertOp || maskOp == mergeOp ) { if ( maskOp == invertOp ) { + cout << " Inverting flattened mask" << endl; for (i = 0; i < nCells; i++){ flattenedMask[i] = (flattenedMask[i] + 1) % 2; } } + cout << " Masking cullCell with the flattened mask" << endl; for ( i = 0; i < nCells; i++ ){ cullCell[i] = max(cullCell[i], flattenedMask[i]); } } else if ( maskOp == preserveOp ) { + cout << " Preserving cells in cullCell with the flattened mask" << endl; for ( i = 0; i < nCells; i++ ) { if ( flattenedMask[i] && cullCell[i] ) { cullCell[i] = 0; From 716119d50d612b52eae81df29a74820f0a6db673 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Aug 2023 18:12:43 +0200 Subject: [PATCH 028/169] Fix widen transect function The mask should be of int type, not float --- conda_package/mpas_tools/ocean/coastline_alteration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_package/mpas_tools/ocean/coastline_alteration.py b/conda_package/mpas_tools/ocean/coastline_alteration.py index 9bc173c21..32318d314 100644 --- a/conda_package/mpas_tools/ocean/coastline_alteration.py +++ b/conda_package/mpas_tools/ocean/coastline_alteration.py @@ -73,7 +73,7 @@ def widen_transect_edge_masks(dsMask, dsMesh, latitude_threshold=43.0): edgeMask[eoc]) # cells with a neighboring transect edge should be masked to 1 dsMask['transectCellMasks'] = dsMask.transectCellMasks.where( - numpy.logical_not(mask), 1.) + numpy.logical_not(mask), 1).astype(int) return dsMask From d89cfc350c1daece8048857a2cca82303e0354bc Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Aug 2023 16:42:34 +0200 Subject: [PATCH 029/169] Fix MOC edge sign and mask --- conda_package/mpas_tools/ocean/moc.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/conda_package/mpas_tools/ocean/moc.py b/conda_package/mpas_tools/ocean/moc.py index cf488cbc0..6013da6ac 100755 --- a/conda_package/mpas_tools/ocean/moc.py +++ b/conda_package/mpas_tools/ocean/moc.py @@ -136,8 +136,6 @@ def _extract_southern_boundary(mesh, mocMask, latBuffer, logger): cellsOnEdgeInRange = numpy.logical_and(cellsOnEdge >= 0, cellsOnEdge < nCells) - # make sure both cells on the dummy edge at the end are out of range - cellsOnEdgeInRange[-1, :] = False southernBoundaryEdges = [] southernBoundaryEdgeSigns = [] @@ -181,7 +179,7 @@ def _extract_southern_boundary(mesh, mocMask, latBuffer, logger): _get_edge_sequence_on_boundary(startEdge, edgeSign, edgesOnVertex, verticesOnEdge) - logger.info(' done: {} edges in transect.'.format(len(edgeSequence))) + logger.info(f' done: {len(edgeSequence)} edges in transect.') aboveSouthernBoundary = latEdge[edgeSequence] > minLat + latBuffer @@ -310,7 +308,7 @@ def _add_transects_to_moc(mesh, mocMask, southernBoundaryEdges, mocMask['regionGroupNames'] = \ (('nRegionGroups',), numpy.zeros((nRegionGroups,), - dtype='|S{}'.format(nChar))) + dtype=f'|S{nChar}')) for index in range(nRegionGroups): mocMask['regionGroupNames'][index] = regionGroupNames[index] @@ -318,7 +316,7 @@ def _add_transects_to_moc(mesh, mocMask, southernBoundaryEdges, # we need to make sure the region names use the same string length mocMask['regionNames'] = \ (('nRegions',), numpy.zeros((nRegions,), - dtype='|S{}'.format(nChar))) + dtype=f'|S{nChar}')) for index in range(nRegions): mocMask['regionNames'][index] = regionNames[index] @@ -368,7 +366,7 @@ def _get_edge_sequence_on_boundary(startEdge, edgeSign, edgesOnVertex, # find the edge that is not iEdge but is on the boundary nextEdge = -1 for edge in eov: - if edge != iEdge and edgeSign[edge] != 0: + if edge != -1 and edge != iEdge and edgeSign[edge] != 0: nextEdge = edge break assert(nextEdge != -1) From 6c3cfcb0743e0daa6faf404cd3677526eda13e8d Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 8 Aug 2023 10:10:50 +0200 Subject: [PATCH 030/169] Update to v0.22.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 975203982..4dfa0ca44 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -34,6 +34,7 @@ Documentation On GitHub `v0.19.0`_ `0.19.0`_ `v0.20.0`_ `0.20.0`_ `v0.21.0`_ `0.21.0`_ +`v0.22.0`_ `0.22.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -92,3 +93,5 @@ Documentation On GitHub .. _`0.20.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.20.0 .. _`v0.21.0`: ../0.21.0/index.html .. _`0.21.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.21.0 +.. _`v0.22.0`: ../0.22.0/index.html +.. _`0.22.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.22.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 9f074532b..591e6e2c8 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 21, 0) +__version_info__ = (0, 22, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 572cc292c..ac5d00b05 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.21.0" %} +{% set version = "0.22.0" %} package: name: {{ name|lower }} From 76dcc515b7491eee822e3b6fa1a1d02190ff6378 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 17 Aug 2023 09:05:33 +0200 Subject: [PATCH 031/169] Update ci packages to match conda-forge versions --- conda_package/ci/linux_python3.10.yaml | 21 ++++++++++++++++----- conda_package/ci/linux_python3.11.yaml | 21 ++++++++++++++++----- conda_package/ci/linux_python3.9.yaml | 21 ++++++++++++++++----- conda_package/ci/osx_python3.10.yaml | 23 ++++++++++++++++++----- conda_package/ci/osx_python3.11.yaml | 23 ++++++++++++++++++----- conda_package/ci/osx_python3.9.yaml | 23 ++++++++++++++++++----- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/conda_package/ci/linux_python3.10.yaml b/conda_package/ci/linux_python3.10.yaml index 5fcfc1b81..6dc07fca9 100644 --- a/conda_package/ci/linux_python3.10.yaml +++ b/conda_package/ci/linux_python3.10.yaml @@ -1,17 +1,25 @@ +cdt_name: +- cos6 channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - gxx cxx_compiler_version: -- '10' +- '12' +docker_image: +- quay.io/condaforge/linux-anvil-cos7-x86_64 fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +28,6 @@ python: - 3.10.* *_cpython target_platform: - linux-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/linux_python3.11.yaml b/conda_package/ci/linux_python3.11.yaml index 662222f89..aec419b8d 100644 --- a/conda_package/ci/linux_python3.11.yaml +++ b/conda_package/ci/linux_python3.11.yaml @@ -1,17 +1,25 @@ +cdt_name: +- cos6 channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - gxx cxx_compiler_version: -- '10' +- '12' +docker_image: +- quay.io/condaforge/linux-anvil-cos7-x86_64 fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +28,6 @@ python: - 3.11.* *_cpython target_platform: - linux-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/linux_python3.9.yaml b/conda_package/ci/linux_python3.9.yaml index 234f1faea..907732c15 100644 --- a/conda_package/ci/linux_python3.9.yaml +++ b/conda_package/ci/linux_python3.9.yaml @@ -1,17 +1,25 @@ +cdt_name: +- cos6 channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - gxx cxx_compiler_version: -- '10' +- '12' +docker_image: +- quay.io/condaforge/linux-anvil-cos7-x86_64 fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +28,6 @@ python: - 3.9.* *_cpython target_platform: - linux-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/osx_python3.10.yaml b/conda_package/ci/osx_python3.10.yaml index 12ad0278a..b6d14d6fb 100644 --- a/conda_package/ci/osx_python3.10.yaml +++ b/conda_package/ci/osx_python3.10.yaml @@ -1,17 +1,27 @@ +MACOSX_DEPLOYMENT_TARGET: +- '10.9' channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - clangxx cxx_compiler_version: -- '14' +- '15' fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +llvm_openmp: +- '15' +macos_machine: +- x86_64-apple-darwin13.4.0 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +30,6 @@ python: - 3.10.* *_cpython target_platform: - osx-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/osx_python3.11.yaml b/conda_package/ci/osx_python3.11.yaml index 456c497f2..20f434f2b 100644 --- a/conda_package/ci/osx_python3.11.yaml +++ b/conda_package/ci/osx_python3.11.yaml @@ -1,17 +1,27 @@ +MACOSX_DEPLOYMENT_TARGET: +- '10.9' channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - clangxx cxx_compiler_version: -- '14' +- '15' fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +llvm_openmp: +- '15' +macos_machine: +- x86_64-apple-darwin13.4.0 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +30,6 @@ python: - 3.11.* *_cpython target_platform: - osx-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/osx_python3.9.yaml b/conda_package/ci/osx_python3.9.yaml index 5d9836c30..965a2b9b7 100644 --- a/conda_package/ci/osx_python3.9.yaml +++ b/conda_package/ci/osx_python3.9.yaml @@ -1,17 +1,27 @@ +MACOSX_DEPLOYMENT_TARGET: +- '10.9' channel_sources: -- conda-forge,defaults +- conda-forge +channel_targets: +- conda-forge main cxx_compiler: - clangxx cxx_compiler_version: -- '14' +- '15' fortran_compiler: - gfortran fortran_compiler_version: -- '10' +- '12' hdf5: -- 1.12.2 +- 1.14.1 libnetcdf: -- 4.8.1 +- 4.9.2 +llvm_openmp: +- '15' +macos_machine: +- x86_64-apple-darwin13.4.0 +netcdf_fortran: +- '4.6' pin_run_as_build: python: min_pin: x.x @@ -20,3 +30,6 @@ python: - 3.9.* *_cpython target_platform: - osx-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version From 8bdcbbbcac1f647893ac0be70d60cfff915ecd6f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 17 Aug 2023 11:43:48 -0500 Subject: [PATCH 032/169] Don't delete temp dir after convert() or cull() --- conda_package/mpas_tools/mesh/conversion.py | 118 ++++++++++---------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/conda_package/mpas_tools/mesh/conversion.py b/conda_package/mpas_tools/mesh/conversion.py index d064d547e..e7055f4cf 100644 --- a/conda_package/mpas_tools/mesh/conversion.py +++ b/conda_package/mpas_tools/mesh/conversion.py @@ -1,6 +1,6 @@ import os import xarray -from tempfile import TemporaryDirectory +from tempfile import TemporaryDirectory, mkdtemp import shutil import mpas_tools.io @@ -38,26 +38,26 @@ def convert(dsIn, graphInfoFileName=None, logger=None, dir=None): """ if dir is not None: dir = os.path.abspath(dir) - with TemporaryDirectory(dir=dir) as tempdir: - inFileName = '{}/mesh_in.nc'.format(tempdir) - write_netcdf(dsIn, inFileName) - outFileName = '{}/mesh_out.nc'.format(tempdir) + tempdir = mkdtemp(dir=dir) + inFileName = '{}/mesh_in.nc'.format(tempdir) + write_netcdf(dsIn, inFileName) - if graphInfoFileName is not None: - graphInfoFileName = os.path.abspath(graphInfoFileName) + outFileName = '{}/mesh_out.nc'.format(tempdir) - outDir = os.path.dirname(outFileName) + if graphInfoFileName is not None: + graphInfoFileName = os.path.abspath(graphInfoFileName) - check_call(['MpasMeshConverter.x', inFileName, outFileName], - logger) + outDir = os.path.dirname(outFileName) - dsOut = xarray.open_dataset(outFileName) - dsOut.load() + check_call(['MpasMeshConverter.x', inFileName, outFileName], logger) - if graphInfoFileName is not None: - shutil.copyfile('{}/graph.info'.format(outDir), - graphInfoFileName) + dsOut = xarray.open_dataset(outFileName) + dsOut.load() + + if graphInfoFileName is not None: + shutil.copyfile('{}/graph.info'.format(outDir), + graphInfoFileName) return dsOut @@ -109,50 +109,50 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, """ if dir is not None: dir = os.path.abspath(dir) - with TemporaryDirectory(dir=dir) as tempdir: - inFileName = '{}/ds_in.nc'.format(tempdir) - write_netcdf(dsIn, inFileName) - outFileName = '{}/ds_out.nc'.format(tempdir) - - args = ['MpasCellCuller.x', inFileName, outFileName] - - if dsMask is not None: - if not isinstance(dsMask, list): - dsMask = [dsMask] - for index, ds in enumerate(dsMask): - fileName = '{}/mask{}.nc'.format(tempdir, index) - write_netcdf(ds, fileName) - args.extend(['-m', fileName]) - - if dsInverse is not None: - if not isinstance(dsInverse, list): - dsInverse = [dsInverse] - for index, ds in enumerate(dsInverse): - fileName = '{}/inverse{}.nc'.format(tempdir, index) - write_netcdf(ds, fileName) - args.extend(['-i', fileName]) - - if dsPreserve is not None: - if not isinstance(dsPreserve, list): - dsPreserve = [dsPreserve] - for index, ds in enumerate(dsPreserve): - fileName = '{}/preserve{}.nc'.format(tempdir, index) - write_netcdf(ds, fileName) - args.extend(['-p', fileName]) - - if graphInfoFileName is not None: - graphInfoFileName = os.path.abspath(graphInfoFileName) - - outDir = os.path.dirname(outFileName) - - check_call(args=args, logger=logger) - - dsOut = xarray.open_dataset(outFileName) - dsOut.load() - - if graphInfoFileName is not None: - shutil.copyfile('{}/culled_graph.info'.format(outDir), - graphInfoFileName) + tempdir = mkdtemp(dir=dir) + inFileName = '{}/ds_in.nc'.format(tempdir) + write_netcdf(dsIn, inFileName) + outFileName = '{}/ds_out.nc'.format(tempdir) + + args = ['MpasCellCuller.x', inFileName, outFileName] + + if dsMask is not None: + if not isinstance(dsMask, list): + dsMask = [dsMask] + for index, ds in enumerate(dsMask): + fileName = '{}/mask{}.nc'.format(tempdir, index) + write_netcdf(ds, fileName) + args.extend(['-m', fileName]) + + if dsInverse is not None: + if not isinstance(dsInverse, list): + dsInverse = [dsInverse] + for index, ds in enumerate(dsInverse): + fileName = '{}/inverse{}.nc'.format(tempdir, index) + write_netcdf(ds, fileName) + args.extend(['-i', fileName]) + + if dsPreserve is not None: + if not isinstance(dsPreserve, list): + dsPreserve = [dsPreserve] + for index, ds in enumerate(dsPreserve): + fileName = '{}/preserve{}.nc'.format(tempdir, index) + write_netcdf(ds, fileName) + args.extend(['-p', fileName]) + + if graphInfoFileName is not None: + graphInfoFileName = os.path.abspath(graphInfoFileName) + + outDir = os.path.dirname(outFileName) + + check_call(args=args, logger=logger) + + dsOut = xarray.open_dataset(outFileName) + dsOut.load() + + if graphInfoFileName is not None: + shutil.copyfile('{}/culled_graph.info'.format(outDir), + graphInfoFileName) return dsOut From 45291efcb8683e5797388143f915d5f44ca9c642 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 17 Aug 2023 08:42:24 +0200 Subject: [PATCH 033/169] Convert masks to int type in `cull()` function The new version of the cell culler requires masks in int (rather than int64 or float) format. --- conda_package/mpas_tools/mesh/conversion.py | 27 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/conda_package/mpas_tools/mesh/conversion.py b/conda_package/mpas_tools/mesh/conversion.py index e7055f4cf..6d25a4149 100644 --- a/conda_package/mpas_tools/mesh/conversion.py +++ b/conda_package/mpas_tools/mesh/conversion.py @@ -1,5 +1,6 @@ import os -import xarray +import numpy as np +import xarray as xr from tempfile import TemporaryDirectory, mkdtemp import shutil @@ -52,7 +53,7 @@ def convert(dsIn, graphInfoFileName=None, logger=None, dir=None): check_call(['MpasMeshConverter.x', inFileName, outFileName], logger) - dsOut = xarray.open_dataset(outFileName) + dsOut = xr.open_dataset(outFileName) dsOut.load() if graphInfoFileName is not None: @@ -111,6 +112,7 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, dir = os.path.abspath(dir) tempdir = mkdtemp(dir=dir) inFileName = '{}/ds_in.nc'.format(tempdir) + dsIn = _masks_to_int(dsIn) write_netcdf(dsIn, inFileName) outFileName = '{}/ds_out.nc'.format(tempdir) @@ -120,6 +122,7 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, if not isinstance(dsMask, list): dsMask = [dsMask] for index, ds in enumerate(dsMask): + ds = _masks_to_int(ds) fileName = '{}/mask{}.nc'.format(tempdir, index) write_netcdf(ds, fileName) args.extend(['-m', fileName]) @@ -128,6 +131,7 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, if not isinstance(dsInverse, list): dsInverse = [dsInverse] for index, ds in enumerate(dsInverse): + ds = _masks_to_int(ds) fileName = '{}/inverse{}.nc'.format(tempdir, index) write_netcdf(ds, fileName) args.extend(['-i', fileName]) @@ -136,6 +140,7 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, if not isinstance(dsPreserve, list): dsPreserve = [dsPreserve] for index, ds in enumerate(dsPreserve): + ds = _masks_to_int(ds) fileName = '{}/preserve{}.nc'.format(tempdir, index) write_netcdf(ds, fileName) args.extend(['-p', fileName]) @@ -147,7 +152,7 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None, check_call(args=args, logger=logger) - dsOut = xarray.open_dataset(outFileName) + dsOut = xr.open_dataset(outFileName) dsOut.load() if graphInfoFileName is not None: @@ -208,7 +213,21 @@ def mask(dsMesh, fcMask=None, logger=None, dir=None, cores=1): check_call(args=args, logger=logger) - dsOut = xarray.open_dataset(outFileName) + dsOut = xr.open_dataset(outFileName) dsOut.load() return dsOut + + +def _masks_to_int(dsIn): + """ Convert masks to int type required by the cell culler """ + var_list = ['regionCellMasks', 'transectCellMasks', 'cullCell', + 'cellSeedMask'] + dsOut = xr.Dataset(dsIn, attrs=dsIn.attrs) + for var in var_list: + if var in dsIn: + print(var) + dsOut[var] = dsIn[var].astype(np.int32) + print(dsOut[var].dtype) + + return dsOut From 2ba6ba6361ed156e7645c9fee9c062ce6ad4c418 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 18 Aug 2023 13:25:28 +0200 Subject: [PATCH 034/169] Update to v0.23.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 4dfa0ca44..e6e127469 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -35,6 +35,7 @@ Documentation On GitHub `v0.20.0`_ `0.20.0`_ `v0.21.0`_ `0.21.0`_ `v0.22.0`_ `0.22.0`_ +`v0.23.0`_ `0.23.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -95,3 +96,5 @@ Documentation On GitHub .. _`0.21.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.21.0 .. _`v0.22.0`: ../0.22.0/index.html .. _`0.22.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.22.0 +.. _`v0.23.0`: ../0.23.0/index.html +.. _`0.23.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.23.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 591e6e2c8..feac2d112 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 22, 0) +__version_info__ = (0, 23, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index ac5d00b05..da351df7c 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.22.0" %} +{% set version = "0.23.0" %} package: name: {{ name|lower }} From e4690e3c9ac702956c683b0eb42823f6dfceb432 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 30 Aug 2023 16:18:52 +0200 Subject: [PATCH 035/169] Fix docs and docstrings --- conda_package/docs/api.rst | 21 +++++++++++++++++---- conda_package/docs/conf.py | 2 +- conda_package/docs/index.rst | 1 + conda_package/docs/ocean/visualization.rst | 2 +- conda_package/docs/seaice/partition.rst | 4 +--- conda_package/mpas_tools/io.py | 5 ++--- conda_package/mpas_tools/ocean/transects.py | 6 +++--- conda_package/mpas_tools/transects.py | 2 +- conda_package/mpas_tools/vector.py | 6 +++--- conda_package/mpas_tools/viz/transects.py | 6 +++--- 10 files changed, 33 insertions(+), 22 deletions(-) diff --git a/conda_package/docs/api.rst b/conda_package/docs/api.rst index c5c89144a..676638d1c 100644 --- a/conda_package/docs/api.rst +++ b/conda_package/docs/api.rst @@ -310,11 +310,24 @@ Transects subdivide_planar lon_lat_to_cartesian cartesian_to_lon_lat - angular_distance - intersects - intersection - Vector +Vector +====== + +.. currentmodule:: mpas_tools.vector + +.. autosummary:: + :toctree: generated/ + + Vector + Vector.angular_distance + Vector.intersects + Vector.intersection + Vector.straddles + Vector.dot + Vector.cross + Vector.det + Vector.mag Visualization ============= diff --git a/conda_package/docs/conf.py b/conda_package/docs/conf.py index 54bb0a4ce..8cf4a6bbd 100644 --- a/conda_package/docs/conf.py +++ b/conda_package/docs/conf.py @@ -81,7 +81,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/conda_package/docs/index.rst b/conda_package/docs/index.rst index 0e8ec2cc0..92de5b6f2 100644 --- a/conda_package/docs/index.rst +++ b/conda_package/docs/index.rst @@ -42,6 +42,7 @@ analyzing simulations, and in other MPAS-related workflows. ocean/coastline_alteration ocean/moc ocean/depth + ocean/visualization .. toctree:: :caption: Sea-ice Tools diff --git a/conda_package/docs/ocean/visualization.rst b/conda_package/docs/ocean/visualization.rst index a2490398c..8f3415613 100644 --- a/conda_package/docs/ocean/visualization.rst +++ b/conda_package/docs/ocean/visualization.rst @@ -104,6 +104,6 @@ If you don't specify the list of variables to plot, all variables with dimensions ``nCells`` and ``nVertLevels`` will be plotted. One way of customizing these visualizaitons is to make your own copy of -`transects.py ` +`transects.py `_ and change ``_plot_transect()`` to suite your needs, (changing figure size, dpi, colorbar, etc.) diff --git a/conda_package/docs/seaice/partition.rst b/conda_package/docs/seaice/partition.rst index f36458741..558b60b93 100644 --- a/conda_package/docs/seaice/partition.rst +++ b/conda_package/docs/seaice/partition.rst @@ -1,4 +1,4 @@ -.. _seaice_partitions: +.. _seaice_partition: Graph partitioning ================== @@ -7,8 +7,6 @@ The :py:mod:`mpas_tools.seaice.partition` module contains a function for creating graph partition files for MPAS-Seaice that are better balanced than those created from Metis tools directly. -.. _seaice_partitions_: - Running from compass -------------------- diff --git a/conda_package/mpas_tools/io.py b/conda_package/mpas_tools/io.py index 9cd410c62..79e0a7526 100644 --- a/conda_package/mpas_tools/io.py +++ b/conda_package/mpas_tools/io.py @@ -33,8 +33,7 @@ def write_netcdf(ds, fileName, fillValues=None, format=None, engine=None, ``mpas_tools.io.default_fills``, which can be modified but which defaults to ``netCDF4.default_fillvals`` - format : {'NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_64BIT', - 'NETCDF3_CLASSIC'}, optional + format : {'NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_64BIT', 'NETCDF3_CLASSIC'}, optional The NetCDF file format to use. Default is ``mpas_tools.io.default_format``, which can be modified but which defaults to ``'NETCDF3_64BIT'`` @@ -49,7 +48,7 @@ def write_netcdf(ds, fileName, fillValues=None, format=None, engine=None, The name of the dimension used for character strings, or None to let xarray figure this out. Default is ``mpas_tools.io.default_char_dim_name``, which can be modified but - which defaults to ``'StrLen'` + which defaults to ``'StrLen'`` """ if format is None: diff --git a/conda_package/mpas_tools/ocean/transects.py b/conda_package/mpas_tools/ocean/transects.py index fef03868e..a8940a971 100644 --- a/conda_package/mpas_tools/ocean/transects.py +++ b/conda_package/mpas_tools/ocean/transects.py @@ -6,7 +6,7 @@ def find_transect_levels_and_weights(dsTransect, layerThickness, bottomDepth, maxLevelCell, zTransect=None): """ Construct a vertical coordinate for a transect produced by - :py:fun:`mpas_tools.viz.transects.find_transect_cells_and_weights()`, then + :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()`, then break each resulting quadrilateral into 2 triangles that can later be visualized with functions like ``tripcolor`` and ``tricontourf``. Also, compute interpolation weights such that observations at points on the @@ -17,7 +17,7 @@ def find_transect_levels_and_weights(dsTransect, layerThickness, bottomDepth, ---------- dsTransect : xarray.Dataset A dataset that defines nodes of the transect, the results of calling - :py:fun:`mpas_tools.viz.transects.find_transect_cells_and_weights()` + :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()` layerThickness : xarray.DataArray layer thicknesses on the MPAS mesh @@ -31,7 +31,7 @@ def find_transect_levels_and_weights(dsTransect, layerThickness, bottomDepth, zTransect : xarray.DataArray, optional the z coordinate of the transect (1D or 2D). If 2D, it must have the same along-transect dimension as the lon and lat passed to - :py:fun:`mpas_tools.viz.transects.find_transect_cells_and_weights()` + :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()` Returns ------- diff --git a/conda_package/mpas_tools/transects.py b/conda_package/mpas_tools/transects.py index 67a186262..ccf195420 100644 --- a/conda_package/mpas_tools/transects.py +++ b/conda_package/mpas_tools/transects.py @@ -224,7 +224,7 @@ def angular_distance(x, y, z): ---------- x : numpy.ndarray The Cartesian x coordinate of a transect, where the number of segments - is ``len(x) - 1``. ``x``, ``y`` and ``z`` are of the same lengt. + is ``len(x) - 1``. ``x``, ``y`` and ``z`` are of the same length. y : numpy.ndarray The Cartesian y coordinate of the transect diff --git a/conda_package/mpas_tools/vector.py b/conda_package/mpas_tools/vector.py index c2914cfe4..1520efbb3 100644 --- a/conda_package/mpas_tools/vector.py +++ b/conda_package/mpas_tools/vector.py @@ -140,12 +140,12 @@ def straddles(a1, a2, b1, b2): Parameters ---------- - a1: mpas_tools.vector.Vector + a1 : mpas_tools.vector.Vector Cartesian coordinates of first end point of first great circle arc. The types of the attributes ``x``, ``y``, and ``z`` must either be ``numpy.arrays`` of identical size for all 4 vectors (in which case intersections are found element-wise), or scalars for - at least one of either the ``a``s or the ``b``s. + at least one of either the a's or the b's. a2 : mpas_tools.vector.Vector Second end point of first great circle arc. @@ -159,7 +159,7 @@ def straddles(a1, a2, b1, b2): Returns ------- straddle : numpy.ndarray - A boolean array of the same size as the ``a``s or the ``b``s, whichever + A boolean array of the same size as the a's or the b's, whichever is greater, indicating if the great circle segment determined by (a1, a2) straddles the great circle determined by (b1, b2) """ diff --git a/conda_package/mpas_tools/viz/transects.py b/conda_package/mpas_tools/viz/transects.py index 2a3aff0d0..c592aff1b 100644 --- a/conda_package/mpas_tools/viz/transects.py +++ b/conda_package/mpas_tools/viz/transects.py @@ -22,7 +22,7 @@ def make_triangle_tree(dsTris): ---------- dsTris : xarray.Dataset A dataset that defines triangles, the results of calling - :py:fun:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` + :py:func:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` Returns ------- @@ -66,7 +66,7 @@ def find_transect_cells_and_weights(lonTransect, latTransect, dsTris, dsMesh, dsTris : xarray.Dataset A dataset that defines triangles, the results of calling - :py:fun:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` + :py:func:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` dsMesh : xarray.Dataset A data set with the full MPAS mesh. @@ -351,7 +351,7 @@ def find_planar_transect_cells_and_weights(xTransect, yTransect, dsTris, dsMesh, dsTris : xarray.Dataset A dataset that defines triangles, the results of calling - `:py:fun:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` + `:py:func:`mpas_tools.viz.mesh_to_triangles.mesh_to_triangles()` dsMesh : xarray.Dataset A data set with the full MPAS mesh. From b584843a4c24eaac9cf89852e4d1aae4976559bf Mon Sep 17 00:00:00 2001 From: Michael Duda Date: Thu, 21 Sep 2023 16:53:11 -0600 Subject: [PATCH 036/169] grid_rotate: Switch from use of 'nc-config' to 'nf-config' in top-level Makefile Beginning with NetCDF-C 4.9.2, the nc-config program no longer provides information from nf-config; see Unidata/netcdf-c@6f55c852 Now, nf-config, rather than nc-config, is used to determine the Fortran compiler, include paths, and library paths to use. --- mesh_tools/grid_rotate/Makefile | 6 +++--- mesh_tools/grid_rotate/README | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mesh_tools/grid_rotate/Makefile b/mesh_tools/grid_rotate/Makefile index 4b92866a2..daf2d3af6 100644 --- a/mesh_tools/grid_rotate/Makefile +++ b/mesh_tools/grid_rotate/Makefile @@ -1,7 +1,7 @@ -FC = $(shell nc-config --fc) +FC = $(shell nf-config --fc) FFLAGS = -O3 -FCINCLUDES = $(shell nc-config --fflags) -FCLIBS = $(shell nc-config --flibs) +FCINCLUDES = $(shell nf-config --fflags) +FCLIBS = $(shell nf-config --flibs) all: grid_rotate diff --git a/mesh_tools/grid_rotate/README b/mesh_tools/grid_rotate/README index 47b4e7209..1d030b0a8 100644 --- a/mesh_tools/grid_rotate/README +++ b/mesh_tools/grid_rotate/README @@ -3,6 +3,9 @@ latitude, longitude, and bird's eye rotations REVISION HISTORY: + 22 Sept 2023 - Switch from nc-config to nf-config for detecting Fortran + compiler and compilation flags + 10 July 2013 - Fix issue where angleEdge was overwritten in output files for meshes that don't provide fEdge and fVertex fields. @@ -27,10 +30,10 @@ II. BUILDING THE CODE Building requires NetCDF and a Fortran compiler. - Before building, ensure that the directory containing the 'nc-config' utility + Before building, ensure that the directory containing the 'nf-config' utility is in your $PATH; this directory is usually in bin/ subdirectory of your - NetCDF installation root directory. The 'nc-config' utility is part of most - modern NetCDF installations, and if you do not have nc-config, it will be + NetCDF installation root directory. The 'nf-config' utility is part of most + modern NetCDF installations, and if you do not have nf-config, it will be necessary to manually set the name of your Fortran compiler, as well as the library paths, in the Makefile; specifically, the following variables must be manually set: FC, FCINCLUDES, and FCLIBS. From f5d153032f6239b9653558ef7b9cb7f7abbeb20f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 26 Sep 2023 22:55:29 +0200 Subject: [PATCH 037/169] Move private method below public --- conda_package/mpas_tools/config.py | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/conda_package/mpas_tools/config.py b/conda_package/mpas_tools/config.py index af2fe8000..9078b5b2b 100644 --- a/conda_package/mpas_tools/config.py +++ b/conda_package/mpas_tools/config.py @@ -423,24 +423,6 @@ def __getitem__(self, section): self.combine() return self.combined[section] - def _add(self, filename, user): - filename = os.path.abspath(filename) - config = RawConfigParser() - if not os.path.exists(filename): - raise FileNotFoundError(f'Config file does not exist: {filename}') - config.read(filenames=filename) - with open(filename) as fp: - comments = self._parse_comments(fp, filename, comments_before=True) - - if user: - self._user_config[filename] = config - else: - self._configs[filename] = config - self._comments[filename] = comments - self.combined = None - self.combined_comments = None - self.sources = None - def combine(self): """ Combine the config files into one. This is normally handled @@ -463,6 +445,24 @@ def combine(self): self.combined_comments[(section, option)] = \ self._comments[source][(section, option)] + def _add(self, filename, user): + filename = os.path.abspath(filename) + config = RawConfigParser() + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file does not exist: {filename}') + config.read(filenames=filename) + with open(filename) as fp: + comments = self._parse_comments(fp, filename, comments_before=True) + + if user: + self._user_config[filename] = config + else: + self._configs[filename] = config + self._comments[filename] = comments + self.combined = None + self.combined_comments = None + self.sources = None + @staticmethod def _parse_comments(fp, filename, comments_before=True): """ Parse the comments in a config file into a dictionary """ From 2b0637114e7da50587361317a1e97a9d6e8f30da Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 26 Sep 2023 23:01:41 +0200 Subject: [PATCH 038/169] Write config files in raw format by default This preserves the formatting for extended interpolation so it is still valid when the config file is read in later. --- conda_package/mpas_tools/config.py | 31 ++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/conda_package/mpas_tools/config.py b/conda_package/mpas_tools/config.py index 9078b5b2b..1b129f5b6 100644 --- a/conda_package/mpas_tools/config.py +++ b/conda_package/mpas_tools/config.py @@ -338,7 +338,7 @@ def set(self, section, option, value=None, comment=None, user=False): comment = ''.join([f'# {line}\n' for line in comment.split('\n')]) self._comments[filename][(section, option)] = comment - def write(self, fp, include_sources=True, include_comments=True): + def write(self, fp, include_sources=True, include_comments=True, raw=True): """ Write the config options to the given file pointer. @@ -354,9 +354,12 @@ def write(self, fp, include_sources=True, include_comments=True): include_comments : bool, optional Whether to include the original comments associated with each section or option + + raw : bool, optional + Whether to write "raw" config options, rather than using extended + interpolation """ - if self.combined is None: - self.combine() + self.combine(raw=raw) for section in self.combined.sections(): section_items = self.combined.items(section=section) if include_comments and section in self.combined_comments: @@ -368,10 +371,17 @@ def write(self, fp, include_sources=True, include_comments=True): if include_sources: source = self.sources[(section, option)] fp.write(f'# source: {source}\n') - value = str(value).replace('\n', '\n\t').replace('$', '$$') + value = str(value).replace('\n', '\n\t') + if not raw: + value = value.replace('$', '$$') fp.write(f'{option} = {value}\n\n') fp.write('\n') + if raw: + # since we combined in "raw" mode, force recombining on future + # access commands + self.combined = None + def list_files(self): """ Get a list of files contributing to the combined config options @@ -423,12 +433,21 @@ def __getitem__(self, section): self.combine() return self.combined[section] - def combine(self): + def combine(self, raw=False): """ Combine the config files into one. This is normally handled automatically. + + Parameters + ---------- + raw : bool, optional + Whether to combine "raw" config options, rather than using extended + interpolation """ - self.combined = ConfigParser(interpolation=ExtendedInterpolation()) + if raw: + self.combined = RawConfigParser() + else: + self.combined = ConfigParser(interpolation=ExtendedInterpolation()) self.sources = dict() self.combined_comments = dict() for configs in [self._configs, self._user_config]: From deca63631d4f09abfd1b62c2ed4b477bac2ab6cc Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 26 Sep 2023 23:12:15 +0200 Subject: [PATCH 039/169] Add append and prepend methods to config parser --- conda_package/mpas_tools/config.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/conda_package/mpas_tools/config.py b/conda_package/mpas_tools/config.py index 1b129f5b6..6d87e9fdc 100644 --- a/conda_package/mpas_tools/config.py +++ b/conda_package/mpas_tools/config.py @@ -415,6 +415,47 @@ def copy(self): config_copy._comments = dict(self._comments) return config_copy + def append(self, other): + """ + Append a deep copy of another config parser to this one. Config + options from ``other`` will take precedence over those from this config + parser. + + Parameters + ---------- + other : mpas_tools.config.MpasConfigParser + The other, higher priority config parser + """ + other = other.copy() + self._configs.update(other._configs) + self._user_config.update(other._user_config) + self._comments.update(other._comments) + + def prepend(self, other): + """ + Prepend a deep copy of another config parser to this one. Config + options from this config parser will take precedence over those from + ``other``. + + Parameters + ---------- + other : mpas_tools.config.MpasConfigParser + The other, higher priority config parser + """ + other = other.copy() + + configs = dict(other._configs) + configs.update(self._configs) + self._configs = configs + + user_config = dict(other._user_config) + user_config.update(self._user_config) + self._user_config = user_config + + comments = dict(other._comments) + comments.update(self._comments) + self._comments = comments + def __getitem__(self, section): """ Get get the config options for a given section. From f38a1bdbb7c41ebbfce16264b1be68416bbf1453 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 28 Sep 2023 00:04:14 +0200 Subject: [PATCH 040/169] Update API --- conda_package/docs/api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda_package/docs/api.rst b/conda_package/docs/api.rst index 676638d1c..0f6a06f88 100644 --- a/conda_package/docs/api.rst +++ b/conda_package/docs/api.rst @@ -116,6 +116,8 @@ Config MpasConfigParser.set MpasConfigParser.write MpasConfigParser.copy + MpasConfigParser.append + MpasConfigParser.prepend MpasConfigParser.__getitem__ I/O From f9b357769d4d3a891b5b410adc0433add792f71f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 28 Sep 2023 09:14:30 +0200 Subject: [PATCH 041/169] Update to v0.24.0 for a release --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index e6e127469..4ea933d7c 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -36,6 +36,7 @@ Documentation On GitHub `v0.21.0`_ `0.21.0`_ `v0.22.0`_ `0.22.0`_ `v0.23.0`_ `0.23.0`_ +`v0.24.0`_ `0.24.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -98,3 +99,5 @@ Documentation On GitHub .. _`0.22.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.22.0 .. _`v0.23.0`: ../0.23.0/index.html .. _`0.23.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.23.0 +.. _`v0.24.0`: ../0.24.0/index.html +.. _`0.24.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.24.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index feac2d112..7fbe2aa81 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 23, 0) +__version_info__ = (0, 24, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index da351df7c..e2bb7034c 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.23.0" %} +{% set version = "0.24.0" %} package: name: {{ name|lower }} From e38b70612dd68e1382a0e938150f2e1a20e55b6e Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 29 Sep 2023 18:23:32 +0200 Subject: [PATCH 042/169] Fix append and prepend for the MpasConfigParser We need to mark the combined config options as invalid after appending or prepending. --- conda_package/mpas_tools/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conda_package/mpas_tools/config.py b/conda_package/mpas_tools/config.py index 6d87e9fdc..256bccf05 100644 --- a/conda_package/mpas_tools/config.py +++ b/conda_package/mpas_tools/config.py @@ -430,6 +430,9 @@ def append(self, other): self._configs.update(other._configs) self._user_config.update(other._user_config) self._comments.update(other._comments) + self.combined = None + self.combined_comments = None + self.sources = None def prepend(self, other): """ @@ -455,6 +458,9 @@ def prepend(self, other): comments = dict(other._comments) comments.update(self._comments) self._comments = comments + self.combined = None + self.combined_comments = None + self.sources = None def __getitem__(self, section): """ From 2d1f15c87bb8db995b7a20e0d7a5dddcd0650761 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 29 Sep 2023 18:48:01 +0200 Subject: [PATCH 043/169] Update to v0.25.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 4ea933d7c..94cc738c3 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -37,6 +37,7 @@ Documentation On GitHub `v0.22.0`_ `0.22.0`_ `v0.23.0`_ `0.23.0`_ `v0.24.0`_ `0.24.0`_ +`v0.25.0`_ `0.25.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -101,3 +102,5 @@ Documentation On GitHub .. _`0.23.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.23.0 .. _`v0.24.0`: ../0.24.0/index.html .. _`0.24.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.24.0 +.. _`v0.25.0`: ../0.25.0/index.html +.. _`0.25.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.25.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 7fbe2aa81..bfa618b5c 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 24, 0) +__version_info__ = (0, 25, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index e2bb7034c..099a4082f 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.24.0" %} +{% set version = "0.25.0" %} package: name: {{ name|lower }} From ecba2b09f439ab9605f5d02d5107ee60891811ed Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 15 Oct 2023 13:41:03 +0200 Subject: [PATCH 044/169] Move vector reconstruction into the conda package This includes a lot of stylistic clean up and adding an entry point `vector_reconstruct`. The old `operators/vector_reconstructions.py` is left as a stub. --- .../{vector.py => vector/__init__.py} | 0 .../mpas_tools/vector/reconstruct.py | 215 ++++++++++++++++++ conda_package/setup.py | 3 +- operators/mpas_tools | 1 + operators/vector_reconstruction.py | 176 +------------- 5 files changed, 219 insertions(+), 176 deletions(-) rename conda_package/mpas_tools/{vector.py => vector/__init__.py} (100%) create mode 100755 conda_package/mpas_tools/vector/reconstruct.py create mode 120000 operators/mpas_tools diff --git a/conda_package/mpas_tools/vector.py b/conda_package/mpas_tools/vector/__init__.py similarity index 100% rename from conda_package/mpas_tools/vector.py rename to conda_package/mpas_tools/vector/__init__.py diff --git a/conda_package/mpas_tools/vector/reconstruct.py b/conda_package/mpas_tools/vector/reconstruct.py new file mode 100755 index 000000000..f3fcfaf2a --- /dev/null +++ b/conda_package/mpas_tools/vector/reconstruct.py @@ -0,0 +1,215 @@ +""" +Extract Cartesian (X, Y, Z), zonal and meridional components of an MPAS +vector field, given the field on edge normals. + +This tool requires that the field 'coeffs_reconstruct' has been saved to a +NetCDF file. The simplest way to do this is to include the following +stream in a forward run: + + + + + + +and run the model for one time step. +""" +import argparse +import sys +from datetime import datetime + +import numpy as np +import xarray as xr +from dask.diagnostics import ProgressBar + +from mpas_tools.io import write_netcdf + + +def reconstruct_variable(out_var_name, variable_on_edges, ds_mesh, + coeffs_reconstruct, ds_out, chunk_size=32768, + quiet=False): + """ + Extract Cartesian (X, Y, Z), zonal and meridional components of an MPAS + vector field, given the field on edge normals. + + Parameters + ---------- + out_var_name : str + The output variable name + + variable_on_edges : xarray.DataArray + The variable at edge normals + + ds_mesh : xarray.Dataset + A dataset with the mesh variables + + coeffs_reconstruct : xarray.DataArray + A data array with the reconstruction coefficients + + ds_out : xarray.Dataset + The dataset the output variables should be added to + + chunk_size : int, optional + The size of chunks along the ``nCells`` dimension used to divide up + the work + + quiet : bool, optional + Whether to suppress print statements and progress bars + """ + n_cells = ds_mesh.sizes['nCells'] + edges_on_cell = ds_mesh.edgesOnCell - 1 + + variable_on_edges.load() + edges_on_cell.load() + coeffs_reconstruct.load() + + dims = [] + sizes = [] + varIndices = {} + for dim in variable_on_edges.dims: + size = variable_on_edges.sizes[dim] + varIndices[dim] = np.arange(size) + if dim == 'nEdges': + dim = 'nCells' + size = n_cells + varIndices['nEdges'] = edges_on_cell + dims.append(dim) + sizes.append(size) + + coeffs_reconstruct = coeffs_reconstruct.chunk({'nCells': chunk_size}) + + variable = variable_on_edges[varIndices].chunk({'nCells': chunk_size}) + if quiet: + variable.compute() + else: + print(f'Computing {out_var_name} at edgesOnCell:') + with ProgressBar(): + variable.compute() + + var_cart = [] + + if not quiet: + print('Computing Cartesian components:') + for index, component in enumerate(['X', 'Y', 'Z']): + var = (coeffs_reconstruct.isel(R3=index)*variable).sum( + dim='maxEdges').transpose(*dims) + out_name = f'{out_var_name}{component}' + if quiet: + var.compute() + else: + print(out_name) + with ProgressBar(): + var.compute() + ds_out[out_name] = var + var_cart.append(var) + + lat_cell = ds_mesh.latCell + lon_cell = ds_mesh.lonCell + lat_cell.load() + lon_cell.load() + + clat = np.cos(lat_cell) + slat = np.sin(lat_cell) + clon = np.cos(lon_cell) + slon = np.sin(lon_cell) + + if not quiet: + print('Computing zonal and meridional components:') + + out_name = f'{out_var_name}Zonal' + zonal = -var_cart[0] * slon + var_cart[1] * clon + if quiet: + zonal.compute() + else: + print(out_name) + with ProgressBar(): + zonal.compute() + ds_out[out_name] = zonal + + out_name = f'{out_var_name}Meridional' + merid = (-(var_cart[0] * clon + var_cart[1] * slon) * slat + + var_cart[2] * clat) + if quiet: + merid.compute() + else: + print(out_name) + with ProgressBar(): + merid.compute() + ds_out[out_name] = merid + + +def main(): + # client = Client(n_workers=1, threads_per_worker=4, memory_limit='10GB') + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument("-m", "--mesh_filename", dest="mesh_filename", + type=str, required=False, + help="An MPAS file with mesh data (edgesOnCell, etc.) " + "if not from in_filename") + parser.add_argument("-w", "--weights_filename", dest="weights_filename", + type=str, required=False, + help="An MPAS file with coeffs_reconstruct if not " + "from in_filename") + parser.add_argument("-i", "--in_filename", dest="in_filename", type=str, + required=True, + help="An MPAS file with one or more fields on edges " + "to be reconstructed at cell centers. Used for " + "mesh data and/or weights if a separate files " + "are not provided.") + parser.add_argument("-v", "--variables", nargs='+', dest="variables", + type=str, required=True, + help="variables on edges to reconstruct") + parser.add_argument("--out_variables", nargs='+', dest="out_variables", + type=str, required=False, + help="prefixes for output variable names") + parser.add_argument("-o", "--out_filename", dest="out_filename", type=str, + required=True, + help="An output MPAS file with the reconstructed " + "X, Y, Z, zonal and meridional fields") + args = parser.parse_args() + + if args.mesh_filename: + mesh_filename = args.mesh_filename + else: + mesh_filename = args.in_filename + + if args.weights_filename: + weights_filename = args.weights_filename + else: + weights_filename = args.in_filename + + if args.out_variables is not None: + out_variables = args.out_variables + else: + out_variables = args.variables + + ds_in = xr.open_dataset(args.in_filename, mask_and_scale=False) + ds_mesh = xr.open_dataset(mesh_filename) + ds_weights = xr.open_dataset(weights_filename) + coeffs_reconstruct = ds_weights.coeffs_reconstruct + ds_out = xr.Dataset() + + for in_var_name, out_var_name in zip(args.variables, out_variables): + reconstruct_variable(out_var_name, ds_in[in_var_name], ds_mesh, + coeffs_reconstruct, ds_out) + + for attr_name in ds_in.attrs: + ds_out.attrs[attr_name] = ds_in.attrs[attr_name] + + time = datetime.now().strftime('%c') + + history = f'{time}: {" ".join(sys.argv)}' + + if 'history' in ds_out.attrs: + ds_out.attrs['history'] = f'{history}\n{ds_out.attrs["history"]}' + else: + ds_out.attrs['history'] = history + + write_netcdf(ds_out, args.out_filename) + + +if __name__ == '__main__': + main() diff --git a/conda_package/setup.py b/conda_package/setup.py index 5506aadcb..99fc36643 100755 --- a/conda_package/setup.py +++ b/conda_package/setup.py @@ -98,4 +98,5 @@ 'compute_projection_region_masks = mpas_tools.mesh.mask:entry_point_compute_projection_grid_region_masks', 'prepare_seaice_partitions = mpas_tools.seaice.partition:prepare_partitions', 'create_seaice_partitions = mpas_tools.seaice.partition:create_partitions', - 'simple_seaice_partitions = mpas_tools.seaice.partition:simple_partitions']}) + 'simple_seaice_partitions = mpas_tools.seaice.partition:simple_partitions', + 'vector_reconstruct = mpas_tools.vector.reconstruct:main']}) diff --git a/operators/mpas_tools b/operators/mpas_tools new file mode 120000 index 000000000..37f8fcddf --- /dev/null +++ b/operators/mpas_tools @@ -0,0 +1 @@ +../conda_package/mpas_tools \ No newline at end of file diff --git a/operators/vector_reconstruction.py b/operators/vector_reconstruction.py index 674b069fd..4a7d013e8 100755 --- a/operators/vector_reconstruction.py +++ b/operators/vector_reconstruction.py @@ -20,181 +20,7 @@ and run the model for one time step. """ -# Authors -# ------- -# Xylar Asay-Davis - -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -import xarray -import numpy -import netCDF4 -import argparse -import sys -from datetime import datetime -from dask.diagnostics import ProgressBar - - -def write_netcdf(ds, fileName, fillValues=netCDF4.default_fillvals): - encodingDict = {} - variableNames = list(ds.data_vars.keys()) + list(ds.coords.keys()) - for variableName in variableNames: - dtype = ds[variableName].dtype - for fillType in fillValues: - if dtype == numpy.dtype(fillType): - encodingDict[variableName] = \ - {'_FillValue': fillValues[fillType]} - break - - delayed_obj = ds.to_netcdf(fileName, encoding=encodingDict, compute=False) - - print('Writing {}'.format(fileName)) - with ProgressBar(): - delayed_obj.compute() - - -def reconstruct_variable(outVarName, variableOnEdges, dsMesh, - coeffs_reconstruct, dsOut, chunkSize=32768): - nCells = dsMesh.sizes['nCells'] - # nEdgesOnCell = dsMesh.nEdgesOnCell.values - edgesOnCell = dsMesh.edgesOnCell - 1 - - variableOnEdges.load() - edgesOnCell.load() - coeffs_reconstruct.load() - - dims = [] - sizes = [] - varIndices = {} - for dim in variableOnEdges.dims: - size = variableOnEdges.sizes[dim] - varIndices[dim] = numpy.arange(size) - if dim == 'nEdges': - dim = 'nCells' - size = nCells - varIndices['nEdges'] = edgesOnCell - dims.append(dim) - sizes.append(size) - - coeffs_reconstruct = coeffs_reconstruct.chunk({'nCells': chunkSize}) - - variable = variableOnEdges[varIndices].chunk({'nCells': chunkSize}) - print('Computing {} at edgesOnCell:'.format(outVarName)) - with ProgressBar(): - variable.compute() - - varCart = [] - - print('Computing Cartesian conponents:') - for index, component in enumerate(['X', 'Y', 'Z']): - var = (coeffs_reconstruct.isel(R3=index)*variable).sum( - dim='maxEdges').transpose(*dims) - outName = '{}{}'.format(outVarName, component) - print(outName) - with ProgressBar(): - var.compute() - dsOut[outName] = var - varCart.append(var) - - latCell = dsMesh.latCell - lonCell = dsMesh.lonCell - latCell.load() - lonCell.load() - - clat = numpy.cos(latCell) - slat = numpy.sin(latCell) - clon = numpy.cos(lonCell) - slon = numpy.sin(lonCell) - - print('Computing zonal and meridional components:') - - outName = '{}Zonal'.format(outVarName) - zonal = -varCart[0]*slon + varCart[1]*clon - print(outName) - with ProgressBar(): - zonal.compute() - dsOut[outName] = zonal - - outName = '{}Meridional'.format(outVarName) - merid = -(varCart[0]*clon + varCart[1]*slon)*slat + varCart[2]*clat - print(outName) - with ProgressBar(): - merid.compute() - dsOut[outName] = merid - - -def main(): - - # client = Client(n_workers=1, threads_per_worker=4, memory_limit='10GB') - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument("-m", "--meshFileName", dest="meshFileName", - type=str, required=False, - help="An MPAS file with mesh data (edgesOnCell, etc.)") - parser.add_argument("-w", "--weightsFileName", dest="weightsFileName", - type=str, required=False, - help="An MPAS file with coeffs_reconstruct ") - parser.add_argument("-i", "--inFileName", dest="inFileName", type=str, - required=True, - help="An MPAS file with one or more fields on edges " - "to be reconstructed at cell centers. Used for " - "mesh data and/or weights if a separate files " - "are not provided.") - parser.add_argument("-v", "--variables", dest="variables", type=str, - required=True, - help="A comma-separated list of variables on edges to " - "reconstruct") - parser.add_argument("--outVariables", dest="outVariables", type=str, - required=False, - help="A comma-separated list of prefixes for output " - "variable names") - parser.add_argument("-o", "--outFileName", dest="outFileName", type=str, - required=True, - help="An output MPAS file with the reconstructed " - "X, Y, Z, zonal and meridional fields") - args = parser.parse_args() - - if args.meshFileName: - meshFileName = args.meshFileName - else: - meshFileName = args.inFileName - - if args.weightsFileName: - weightsFileName = args.weightsFileName - else: - weightsFileName = args.inFileName - - variables = args.variables.split(',') - if args.outVariables: - outVariables = args.outVariables.split(',') - else: - outVariables = variables - - dsIn = xarray.open_dataset(args.inFileName, mask_and_scale=False) - dsMesh = xarray.open_dataset(meshFileName) - dsWeights = xarray.open_dataset(weightsFileName) - coeffs_reconstruct = dsWeights.coeffs_reconstruct - dsOut = xarray.Dataset() - - for inVarName, outVarName in zip(variables, outVariables): - reconstruct_variable(outVarName, dsIn[inVarName], dsMesh, - coeffs_reconstruct, dsOut) - - for attrName in dsIn.attrs: - dsOut.attrs[attrName] = dsIn.attrs[attrName] - - time = datetime.now().strftime('%c') - - history = '{}: {}'.format(time, ' '.join(sys.argv)) - - if 'history' in dsOut.attrs: - dsOut.attrs['history'] = '{}\n{}'.format(history, - dsOut.attrs['history']) - else: - dsOut.attrs['history'] = history - - write_netcdf(dsOut, args.outFileName) +from mpas_tools.vector.reconstruct import main if __name__ == '__main__': From d743aa8ddb9fd91859353637241dc1bc880787ad Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 15 Oct 2023 13:42:51 +0200 Subject: [PATCH 045/169] Update docs with vector class and vector reconstruction --- conda_package/docs/index.rst | 2 ++ conda_package/docs/vector.rst | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 conda_package/docs/vector.rst diff --git a/conda_package/docs/index.rst b/conda_package/docs/index.rst index 92de5b6f2..f2866cd66 100644 --- a/conda_package/docs/index.rst +++ b/conda_package/docs/index.rst @@ -31,6 +31,8 @@ analyzing simulations, and in other MPAS-related workflows. transects + vector + visualization .. toctree:: diff --git a/conda_package/docs/vector.rst b/conda_package/docs/vector.rst new file mode 100644 index 000000000..11520613b --- /dev/null +++ b/conda_package/docs/vector.rst @@ -0,0 +1,63 @@ +.. _vector: + +***************** +Vector Operations +***************** + +MPAS-Tools has a ``Vector`` class and an unrelated tool for performing +vector reconstructions at cell centers from fields defined at edge normals. + +.. _vector_class: + +Vector Class +============ + +The :py:class:`mpas_tools.vector.Vector` class defines a single vector (with +components that are floats) or a vector field (with components that are +:py:class:`numpy.ndarray` objects). See the API documentation for the +individual methods to find out more. + + +.. _vector_reconstruct: + +Vector Reconstructions +====================== + +The command-line tool ``vector_reconstruct`` and the function +:py:func:`mpas_tools.vector.reconstruct.reconstruct_variable()` are used to +reconstruct Cartesian (X, Y, Z), zonal and meridional components of an MPAS +vector field at cell centers, given the field on edge normals. + +This tool requires that the field ``coeffs_reconstruct`` has been saved to a +NetCDF file. The simplest way to do this is to include the following +stream in a forward run: + +.. code-block:: xml + + + + + + +and run the model for one time step. + +Then, ``vector_reconstruct`` is called with: + +.. code-block:: + + $ vector_reconstruct --help + usage: vector_reconstruct [-h] [-m MESH_FILENAME] [-w WEIGHTS_FILENAME] -i + IN_FILENAME -v VARIABLES [VARIABLES ...] + [--out_variables OUT_VARIABLES [OUT_VARIABLES ...]] + -o OUT_FILENAME + +You must supply input and output files and a list of one or more variables on +edge normals to reconstruct. You can optionally supply a separate file that +contains the MPAS mesh if it is not part of the input file, a file with +``coeffs_reconstruct`` if it is not in the input file, and a list of variable +prefixes corresponding to each variable supplied that should be prepended to +the Cartesian, zonal and meridional reconstructions for that variable. From 1c0fd0efab413582c3ab6be651955e219698b7a5 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 15 Oct 2023 13:46:21 +0200 Subject: [PATCH 046/169] Update to v0.26.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 94cc738c3..dd76cb796 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -38,6 +38,7 @@ Documentation On GitHub `v0.23.0`_ `0.23.0`_ `v0.24.0`_ `0.24.0`_ `v0.25.0`_ `0.25.0`_ +`v0.26.0`_ `0.26.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -104,3 +105,5 @@ Documentation On GitHub .. _`0.24.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.24.0 .. _`v0.25.0`: ../0.25.0/index.html .. _`0.25.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.25.0 +.. _`v0.26.0`: ../0.26.0/index.html +.. _`0.26.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.26.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index bfa618b5c..8de357239 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 25, 0) +__version_info__ = (0, 26, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 099a4082f..e5dade035 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.25.0" %} +{% set version = "0.26.0" %} package: name: {{ name|lower }} From 6e1ccbe0e9e90c8c8feeb1bb292b4f0bd53fd581 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 28 Oct 2023 12:25:45 +0200 Subject: [PATCH 047/169] Fix sorting of transect intersections Sometimes, when a segment of a transect ends exactly at an intersection with a cell edge, there are redundant intersection points in the list. This merge removes those redundant points. This merge also fixes some index arrays that were previously type float but should be int. --- conda_package/mpas_tools/viz/transects.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/conda_package/mpas_tools/viz/transects.py b/conda_package/mpas_tools/viz/transects.py index c592aff1b..0d9670b47 100644 --- a/conda_package/mpas_tools/viz/transects.py +++ b/conda_package/mpas_tools/viz/transects.py @@ -1,6 +1,6 @@ +import matplotlib.pyplot as plt import numpy import xarray - from scipy.spatial import cKDTree from shapely.geometry import LineString, Point @@ -516,9 +516,9 @@ def find_planar_transect_cells_and_weights(xTransect, yTransect, dsTris, dsMesh, xIntersection = numpy.array(xIntersection) yIntersection = numpy.array(yIntersection) nodeWeights = numpy.array(nodeWeights) - node0Inter = numpy.array(node0Inter) - node1Inter = numpy.array(node1Inter) - trisInter = numpy.array(trisInter) + node0Inter = numpy.array(node0Inter, dtype=int) + node1Inter = numpy.array(node1Inter, dtype=int) + trisInter = numpy.array(trisInter, dtype=int) dNodeLocal = dStart + distances @@ -626,6 +626,17 @@ def _sort_intersections(dNode, tris, nodes, xOut, yOut, zOut, interpCells, dSorted = dNode[sortIndices] trisSorted = tris[sortIndices] + # sometimes we end up with redundant intersections in the transect, and + # these need to be removed + unique_d_tris = dict() + for index in range(len(dSorted)): + unique_d_tris[(dSorted[index], trisSorted[index])] = index + + unique_indices = list(unique_d_tris.values()) + sortIndices = sortIndices[unique_indices] + dSorted = dSorted[unique_indices] + trisSorted = trisSorted[unique_indices] + nodesAreSame = numpy.abs(dSorted[1:] - dSorted[:-1]) < epsilon if nodesAreSame[0]: # the first two nodes are the same, so the first transect point is in a From 525e37648c014d0441926aac9d5d1e3fb3210054 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 28 Oct 2023 12:27:46 +0200 Subject: [PATCH 048/169] Fix interpolation of SSH to transects This merge fixes masking of the SSH to ignore cells where maxLevelCell=0 --- conda_package/mpas_tools/ocean/transects.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/conda_package/mpas_tools/ocean/transects.py b/conda_package/mpas_tools/ocean/transects.py index a8940a971..bf6a140f3 100644 --- a/conda_package/mpas_tools/ocean/transects.py +++ b/conda_package/mpas_tools/ocean/transects.py @@ -378,9 +378,14 @@ def _get_vertical_coordinate(dsTransect, layerThickness, bottomDepth, layerThicknessTransect = (layerThicknessTransect*interpCellWeights).sum( dim='nHorizWeights') + interpMask = maxLevelCell.isel(nCells=interpCellIndices) >= 0 + interpHorizCellWeights = interpMask*dsTransect.interpHorizCellWeights + weightSum = interpHorizCellWeights.sum(dim='nHorizWeights') + interpHorizCellWeights = \ + (interpHorizCellWeights/weightSum).where(interpMask) + sshTransect = ssh.isel(nCells=interpCellIndices) - sshTransect = (sshTransect*dsTransect.interpHorizCellWeights).sum( - dim='nHorizWeights') + sshTransect = (sshTransect*interpHorizCellWeights).sum(dim='nHorizWeights') zBot = sshTransect - layerThicknessTransect.cumsum(dim='nVertLevels') zTop = zBot + layerThicknessTransect From 949f153532c0df742e3d0a886bdbfda43b7c725f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 28 Oct 2023 13:46:55 +0200 Subject: [PATCH 049/169] Add back the `MpasMaskCreator.x` tool This tool is still needed for certain types of transect plots where an ordered list of edges is required. --- conda_package/recipe/build.sh | 11 +++++++++++ conda_package/recipe/meta.yaml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/conda_package/recipe/build.sh b/conda_package/recipe/build.sh index 870739f0e..80a444ddd 100644 --- a/conda_package/recipe/build.sh +++ b/conda_package/recipe/build.sh @@ -19,6 +19,17 @@ cmake \ cmake --build . cmake --install . +# build and install legacy mask creator +cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools +mkdir build +cd build +cmake \ + -D CMAKE_INSTALL_PREFIX=${PREFIX} \ + -D CMAKE_BUILD_TYPE=Release \ + .. +cmake --build . +cp MpasMaskCreator.x ${PREFIX}/bin + # build and install mesh conversion tools cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools_netcdf_c mkdir build diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index e5dade035..1181a15fc 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -104,7 +104,7 @@ test: - MpasMeshConverter.x mesh_tools/mesh_conversion_tools/test/mesh.QU.1920km.151026.nc mesh.nc - sort_mesh --mesh-file mesh.nc --sort-file sorted_mesh.nc - MpasCellCuller.x mesh.nc culled_mesh.nc -m mesh_tools/mesh_conversion_tools/test/land_mask_final.nc - # - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson + - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson - planar_hex --nx=30 --ny=20 --dc=1000. --npx --npy --outFileName='nonperiodic_mesh_30x20_1km.nc' - MpasCellCuller.x nonperiodic_mesh_30x20_1km.nc culled_nonperiodic_mesh_30x20_1km.nc - python -m pytest conda_package/mpas_tools/tests From a4498704ac6aebbc9bb36e3a19531e0ed5252597 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 28 Oct 2023 15:05:39 +0200 Subject: [PATCH 050/169] Update to v0.27.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index dd76cb796..2a8a6af02 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -39,6 +39,7 @@ Documentation On GitHub `v0.24.0`_ `0.24.0`_ `v0.25.0`_ `0.25.0`_ `v0.26.0`_ `0.26.0`_ +`v0.27.0`_ `0.27.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -107,3 +108,5 @@ Documentation On GitHub .. _`0.25.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.25.0 .. _`v0.26.0`: ../0.26.0/index.html .. _`0.26.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.26.0 +.. _`v0.27.0`: ../0.27.0/index.html +.. _`0.27.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.27.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 8de357239..2b9ef9c8e 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 26, 0) +__version_info__ = (0, 27, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 1181a15fc..8aa8a884b 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.26.0" %} +{% set version = "0.27.0" %} package: name: {{ name|lower }} From 6ce1253b90ec4939363d484c02c33437ac1ecd7e Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 17 Nov 2023 14:16:04 -0700 Subject: [PATCH 051/169] Add ocean topography smoothing tool --- conda_package/recipe/build.sh | 12 +- ocean/smooth_topo/CMakeLists.txt | 17 + ocean/smooth_topo/README | 19 + ocean/smooth_topo/cmake/FindNetCDF.cmake | 71 ++ ocean/smooth_topo/cmake/LICENSE | 23 + ocean/smooth_topo/cmake/README | 1 + .../smooth_topo/smooth_topo_skip_land_ice.F90 | 800 ++++++++++++++++++ 7 files changed, 942 insertions(+), 1 deletion(-) create mode 100644 ocean/smooth_topo/CMakeLists.txt create mode 100644 ocean/smooth_topo/README create mode 100644 ocean/smooth_topo/cmake/FindNetCDF.cmake create mode 100644 ocean/smooth_topo/cmake/LICENSE create mode 100644 ocean/smooth_topo/cmake/README create mode 100644 ocean/smooth_topo/smooth_topo_skip_land_ice.F90 diff --git a/conda_package/recipe/build.sh b/conda_package/recipe/build.sh index 80a444ddd..cf69c237b 100644 --- a/conda_package/recipe/build.sh +++ b/conda_package/recipe/build.sh @@ -8,6 +8,17 @@ cp -r ocean landice visualization mesh_tools conda_package cd conda_package ${PYTHON} -m pip install . --no-deps -vv +# build and install ocean topography smoothing tool +cd ${SRC_DIR}/conda_package/ocean/smooth_topo +mkdir build +cd build +cmake \ + -D CMAKE_INSTALL_PREFIX=${PREFIX} \ + -D CMAKE_BUILD_TYPE=Release \ + .. +cmake --build . +cmake --install . + # build and install sea ice partitioning tool cd ${SRC_DIR}/conda_package/mesh_tools/seaice_grid_tools mkdir build @@ -40,4 +51,3 @@ cmake \ .. cmake --build . cmake --install . - diff --git a/ocean/smooth_topo/CMakeLists.txt b/ocean/smooth_topo/CMakeLists.txt new file mode 100644 index 000000000..a06d7a180 --- /dev/null +++ b/ocean/smooth_topo/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required (VERSION 3.0.2) +enable_language(Fortran) +project (ocean_smooth_topo) + +list (APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) + +set (NETCDF_F90 "YES") +find_package (NetCDF REQUIRED) + +message (STATUS "NETCDF_INCLUDES=${NETCDF_INCLUDES}") +message (STATUS "NETCDF_LIBRARIES=${NETCDF_LIBRARIES}") + +include_directories(${NETCDF_INCLUDES}) +add_executable (ocean_smooth_topo_skip_land_ice smooth_topo_skip_land_ice.F90) +target_link_libraries (ocean_smooth_topo_skip_land_ice ${NETCDF_LIBRARIES}) + +install (TARGETS ocean_smooth_topo_skip_land_ice DESTINATION bin) diff --git a/ocean/smooth_topo/README b/ocean/smooth_topo/README new file mode 100644 index 000000000..19384f482 --- /dev/null +++ b/ocean/smooth_topo/README @@ -0,0 +1,19 @@ +Authors: Adrian Turner, Mat Maltrud + +A tool for smoothing bathymetry in an MPAS-Ocean initial condition with a +Gaussian filter with a characteristic length scale (stdDeviation) and cut-off +distance (distanceLimit). The smoothing is only applied if the original +maxLevelCell is larger than a given threshold (minLevelForSmoothing). Smoothing +is applied over a given number of iterations (numSmoothingPasses). + +An example namelist file (smooth_depth_in) is: + +&smooth + filename_depth_in = "mpaso.IcoswISC30E3r2.20230901.nc" + filename_depth_out = "smooth.IcoswISC30E3r2.r200e100.1pass.min5.skipLandIce.231115.nc" + filename_mpas_mesh = "mpaso.IcoswISC30E3r2.20230901.nc" + distanceLimit = 200. + stdDeviation = 100. + numSmoothingPasses = 1 + minLevelForSmoothing = 5 +/ diff --git a/ocean/smooth_topo/cmake/FindNetCDF.cmake b/ocean/smooth_topo/cmake/FindNetCDF.cmake new file mode 100644 index 000000000..4db4b0d38 --- /dev/null +++ b/ocean/smooth_topo/cmake/FindNetCDF.cmake @@ -0,0 +1,71 @@ +# - Find NetCDF +# Find the native NetCDF includes and library +# +# NETCDF_INCLUDES - where to find netcdf.h, etc +# NETCDF_LIBRARIES - Link these libraries when using NetCDF +# NETCDF_FOUND - True if NetCDF found including required interfaces (see below) +# +# Your package can require certain interfaces to be FOUND by setting these +# +# NETCDF_CXX - require the C++ interface and link the C++ library +# NETCDF_F77 - require the F77 interface and link the fortran library +# NETCDF_F90 - require the F90 interface and link the fortran library +# +# The following are not for general use and are included in +# NETCDF_LIBRARIES if the corresponding option above is set. +# +# NETCDF_LIBRARIES_C - Just the C interface +# NETCDF_LIBRARIES_CXX - C++ interface, if available +# NETCDF_LIBRARIES_F77 - Fortran 77 interface, if available +# NETCDF_LIBRARIES_F90 - Fortran 90 interface, if available +# +# Normal usage would be: +# set (NETCDF_F90 "YES") +# find_package (NetCDF REQUIRED) +# target_link_libraries (uses_f90_interface ${NETCDF_LIBRARIES}) +# target_link_libraries (only_uses_c_interface ${NETCDF_LIBRARIES_C}) + +if (NETCDF_INCLUDES AND NETCDF_LIBRARIES) + # Already in cache, be silent + set (NETCDF_FIND_QUIETLY TRUE) +endif (NETCDF_INCLUDES AND NETCDF_LIBRARIES) + +find_path (NETCDF_INCLUDES netcdf.h + HINTS NETCDF_DIR ENV NETCDF_DIR) + +find_library (NETCDF_LIBRARIES_C NAMES netcdf) +mark_as_advanced(NETCDF_LIBRARIES_C) + +set (NetCDF_has_interfaces "YES") # will be set to NO if we're missing any interfaces +set (NetCDF_libs "${NETCDF_LIBRARIES_C}") + +get_filename_component (NetCDF_lib_dirs "${NETCDF_LIBRARIES_C}" PATH) + +macro (NetCDF_check_interface lang header libs) + if (NETCDF_${lang}) + find_path (NETCDF_INCLUDES_${lang} NAMES ${header} + HINTS "${NETCDF_INCLUDES}" NO_DEFAULT_PATH) + find_library (NETCDF_LIBRARIES_${lang} NAMES ${libs} + HINTS "${NetCDF_lib_dirs}" NO_DEFAULT_PATH) + mark_as_advanced (NETCDF_INCLUDES_${lang} NETCDF_LIBRARIES_${lang}) + if (NETCDF_INCLUDES_${lang} AND NETCDF_LIBRARIES_${lang}) + list (INSERT NetCDF_libs 0 ${NETCDF_LIBRARIES_${lang}}) # prepend so that -lnetcdf is last + else (NETCDF_INCLUDES_${lang} AND NETCDF_LIBRARIES_${lang}) + set (NetCDF_has_interfaces "NO") + message (STATUS "Failed to find NetCDF interface for ${lang}") + endif (NETCDF_INCLUDES_${lang} AND NETCDF_LIBRARIES_${lang}) + endif (NETCDF_${lang}) +endmacro (NetCDF_check_interface) + +NetCDF_check_interface (CXX netcdfcpp.h netcdf_c++) +NetCDF_check_interface (F77 netcdf.inc netcdff) +NetCDF_check_interface (F90 netcdf.mod netcdff) + +set (NETCDF_LIBRARIES "${NetCDF_libs}" CACHE STRING "All NetCDF libraries required for interface level") + +# handle the QUIETLY and REQUIRED arguments and set NETCDF_FOUND to TRUE if +# all listed variables are TRUE +include (FindPackageHandleStandardArgs) +find_package_handle_standard_args (NetCDF DEFAULT_MSG NETCDF_LIBRARIES NETCDF_INCLUDES NetCDF_has_interfaces) + +mark_as_advanced (NETCDF_LIBRARIES NETCDF_INCLUDES) diff --git a/ocean/smooth_topo/cmake/LICENSE b/ocean/smooth_topo/cmake/LICENSE new file mode 100644 index 000000000..49bac9892 --- /dev/null +++ b/ocean/smooth_topo/cmake/LICENSE @@ -0,0 +1,23 @@ +Copyright $(git shortlog -s) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ocean/smooth_topo/cmake/README b/ocean/smooth_topo/cmake/README new file mode 100644 index 000000000..b4811763c --- /dev/null +++ b/ocean/smooth_topo/cmake/README @@ -0,0 +1 @@ +These files are from https://github.com/jedbrown/cmake-modules diff --git a/ocean/smooth_topo/smooth_topo_skip_land_ice.F90 b/ocean/smooth_topo/smooth_topo_skip_land_ice.F90 new file mode 100644 index 000000000..9bbc33f0c --- /dev/null +++ b/ocean/smooth_topo/smooth_topo_skip_land_ice.F90 @@ -0,0 +1,800 @@ + +program smooth_topography + +! on compy +! module load gcc;module load netcdf +!gfortran smooth_topo.F90 -o a.out -I /share/apps/netcdf/4.6.3/gcc/4.8.5/include/ -L /share/apps/netcdf/4.6.3/gcc/4.8.5/lib/ -lnetcdf -lnetcdff + + use netcdf + + implicit none + + integer, parameter :: & + RKIND = selected_real_kind(13) + + ! input parameters + character(len=1000) :: & + filename_depth_in, & + filename_depth_out, & + filename_mpas_mesh, & + filename_diagnostic + + real(kind=RKIND) :: & + layerBottom, dz, & + layerTop, partialCell_dz, & + maxBottomThicknessOrig, & + distanceLimit, & ! (km) + stdDeviation ! (km) + + integer :: & + numSmoothingPasses, smoothingPass, minLevelForSmoothing + + namelist /smooth/ & + filename_depth_in, & + filename_depth_out, & + filename_mpas_mesh, & + distanceLimit, & + stdDeviation, & + numSmoothingPasses, & + minLevelForSmoothing + + real(kind=RKIND), dimension(:,:), allocatable :: & + restingThicknessOrig, & + restingThicknessNew + + ! mpas grid + integer :: & + nCells, & + nEdges, & + nVertLevels, & + iCellFull, & + maxEdges + + real(kind=RKIND), dimension(:), allocatable :: & + xCell, & + yCell, & + zCell, & + lonCell, & + latCell, & + areaCell, & + bottomDepthOrig, & + bottomDepthNew, & + bottomDepthPrev, & + dcEdge + + integer, dimension(:), allocatable :: & + landIceMask, & + maxLevelCellOrig, & + maxLevelCellNew, & + nEdgesOnCell + + integer, dimension(:,:), allocatable :: & + cellsOnCell, & + edgesOnCell + + ! temporary variables + integer :: & + is, & + ia, & + iCell, & + k, & + start, & + count, & + ncid, & + varid_landIceMask, & + varid_DepthOrig, & + varid_DepthNew, & + varid_maxLevCellOrig, & + varid_maxLevCellNew, & + varid_RestThickOrig, & + varid_RestThickNew + + real(kind=RKIND), dimension(:), allocatable :: & + weights, & + cellDistance, & + distanceSpread + + integer, dimension(:), allocatable :: & + iCellsSpread, & + cellStatus + + real(kind=RKIND), parameter :: & + km_to_m = 1000.0_RKIND + + ! diagnostic variables + real(kind=RKIND), dimension(:), allocatable :: & + weightsDiagnostic + + ! read in namelist + write(*,*) "Reading namelist..." + open(11,file="smooth_depth_in",status='old') + read(11,nml=smooth) + close(11) + + ! load the MPAS mesh file + call load_mesh_file() + + ! allocate temporary arrays + write(*,*) "Allocate temporary variables..." + allocate(weights(nCells)) + allocate(cellDistance(nCells)) + allocate(distanceSpread(nCells)) + allocate(iCellsSpread(nCells)) + allocate(cellStatus(nCells)) + + write(*,*) "Perform smoothing..." + + bottomDepthPrev = bottomDepthOrig + bottomDepthNew = bottomDepthOrig ! for cells that do not get smoothed + +! convert to degrees + latCell = latCell*180./3.14159 + lonCell = lonCell*180./3.14159 + + do smoothingPass = 1, numSmoothingPasses + + write(*,*)' smoothing pass ',smoothingPass + + ! perform the smoothing + do iCell = 1, nCells + +! only smooth sufficiently deep cells + if (maxLevelCellOrig(iCell) >= minLevelForSmoothing) then + call smooth_topo(& + iCell, & + distanceLimit * km_to_m, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceSpread) + endif + + enddo ! iCell + + bottomDepthPrev = bottomDepthNew + enddo ! smoothingPass + + ! find maximum thickness in bottom layer + maxBottomThicknessOrig = -1.0 + do iCell = 1, nCells + if (restingThicknessOrig(nVertLevels,iCell) > maxBottomThicknessOrig) & + maxBottomThicknessOrig = restingThicknessOrig(nVertLevels,iCell) + enddo + + ! find first column that extends to the bottom + do iCell = 1, nCells + if (maxLevelCellOrig(iCell) == nVertLevels .and. & + restingThicknessOrig(nVertLevels,iCell) == maxBottomThicknessOrig) then + iCellFull = iCell + exit + endif + enddo + + ! find new maxLevelCell and restingThickness + do iCell = 1, nCells + if (landIceMask(iCell) == 0) then + maxLevelCellNew(iCell) = nVertLevels ! some deep levels get set to zero due to roundoff + layerBottom = 0.0_RKIND + layerTop = 0.0_RKIND + do k = 1, nVertLevels + restingThicknessNew(k,iCell) = 0.0_RKIND + layerBottom = layerBottom + restingThicknessOrig(k,iCellFull) + dz = bottomDepthNew(iCell) - layerBottom + restingThicknessNew(k,iCell) = restingThicknessOrig(k,iCellFull) + + if (dz < 0.0_RKIND) then + partialCell_dz = bottomDepthNew(iCell) - layerTop + if ( partialCell_dz < 0.25*restingThicknessOrig(k,iCellFull) ) then + bottomDepthNew(iCell) = layerTop + 0.25*restingThicknessOrig(k,iCellFull) + restingThicknessNew(k,iCell) = 0.25*restingThicknessOrig(k,iCellFull) + else + restingThicknessNew(k,iCell) = abs(partialCell_dz) ! abs for roundoff + endif + maxLevelCellNew(iCell) = k + + exit + endif + layerTop = layerBottom + enddo ! k + + else ! landIceMask = 1 + bottomDepthNew(iCell) = bottomDepthOrig(iCell) + restingThicknessNew(:,iCell) = restingThicknessOrig(:,iCell) + maxLevelCellNew(iCell) = maxLevelCellOrig(iCell) + endif + + enddo ! iCell + + ! output the depth file + call create_depth_file(filename_depth_out, ncid, & + varid_DepthOrig, varid_DepthNew, varid_maxLevCellOrig, varid_maxLevCellNew, & + varid_RestThickOrig, varid_RestThickNew) + + ! close the depth file + call close_depth_file(ncid) + +contains + + !---------------------------------------------------------------- + + function gaussian_weight(distance, stdDeviation) result(weight) + + real(kind=RKIND), intent(in) :: & + distance, & + stdDeviation + + real(kind=RKIND) :: weight + + weight = exp(-1.0_RKIND * (distance**2 / (2.0_RKIND * stdDeviation**2))) + + end function gaussian_weight + + !---------------------------------------------------------------- + + subroutine smooth_topo(& + iCell_Start, & + distanceLimit, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceOut) + + integer, intent(in) :: & + iCell_Start + + real(kind=RKIND), intent(in) :: & + distanceLimit + + integer, dimension(:), intent(in) :: & + nEdgesOnCell + + integer, dimension(:,:), intent(in) :: & + cellsOnCell, & + edgesOnCell + + real(kind=RKIND), dimension(:), intent(in) :: & + dcEdge + + integer, dimension(:), intent(out) :: & + cellStatus + + real(kind=RKIND), dimension(:), intent(out) :: & + cellDistance, & + distanceOut + + integer :: & + nCellsOut + + integer, dimension(:), allocatable :: & + iCellsPrev, & + iCellsOut, & + iCellsNext + + integer :: & + nCellsPrev, & + nCellsNext, & + nLayer, & + iCellPrev, & + iCellOnCell, & + iCellNext, & + iEdge + + real(kind=RKIND) :: & + cellDistanceToAdd, & + weight, & + smoothedTopoSum, & + weightSum + + allocate(iCellsPrev(nCells)) + allocate(iCellsNext(nCells)) + allocate(iCellsOut(nCells)) + + ! set the internal variables + cellStatus = -1 + cellDistance = -1.0_RKIND + + ! set first cell + cellStatus(iCell_Start) = 0 + nCellsPrev = 1 + iCellsPrev(1) = iCell_Start + + ! first output cell is the starting cell + nCellsOut = 1 + iCellsOut(1) = iCell_Start + distanceOut(1) = 0.0_RKIND + + ! initialize sums + smoothedTopoSum = 0.0_RKIND + weightSum = 0.0_RKIND + + ! loop over cell layers from original cell + nLayer = 0 + do while (nCellsPrev > 0) + + ! reset number of cells found this layer + nCellsNext = 0 + + ! loop over cells defined in the previous iteration + do iCellPrev = 1, nCellsPrev + + ! loop over neighbours of these previous cells + do iCellOnCell = 1, nEdgesOnCell(iCellsPrev(iCellPrev)) + + ! get the iCell of the next potential cell in the next cells + iCellNext = cellsOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + ! get the edge index for this crossing + iEdge = edgesOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + cellDistanceToAdd = cellDistance(iCellsPrev(iCellPrev)) + dcEdge(iEdge) + + ! check to see if we need to add it to the next array + if (landIceMask(iCellNext) == 0 .and. cellStatus(iCellNext) == -1 .and. cellDistanceToAdd < distanceLimit) then + + ! count how many on the next list + nCellsNext = nCellsNext + 1 + + ! add this new cell to the next list + iCellsNext(nCellsNext) = iCellNext + + ! update the status of the cell + cellStatus(iCellNext) = nLayer + + ! calculate the distance to this cell + cellDistance(iCellNext) = cellDistanceToAdd + + ! output + nCellsOut = nCellsOut + 1 + iCellsOut(nCellsOut) = iCellNext + distanceOut(nCellsOut) = cellDistanceToAdd + + weight = gaussian_weight(cellDistanceToAdd, stdDeviation * km_to_m) + weightSum = weightSum + weight + smoothedTopoSum = smoothedTopoSum + weight*bottomDepthPrev(iCellNext) + + endif ! cellStatus(iCellNext) == -1 + + enddo ! iCellOnCell + + enddo ! iCellPrev + + ! swap next and prev + nCellsPrev = nCellsNext + + iCellsPrev(1:nCellsNext) = iCellsnext(1:nCellsNext) + + ! increment the layer number + nLayer = nLayer + 1 + + enddo ! nCellsNext > 0 + + bottomDepthNew(iCell_Start) = smoothedTopoSum/weightSum + + deallocate(iCellsPrev) + deallocate(iCellsNext) + + end subroutine smooth_topo + + !---------------------------------------------------------------- + + subroutine create_depth_file(filename, ncid, & + varid_DepthOrig, varid_DepthNew, varid_maxLevCellOrig, varid_maxLevCellNew, & + varid_RestThickOrig, varid_RestThickNew) + + character(len=*), intent(in) :: & + filename + + integer, intent(out) :: & + ncid, & + varid_DepthOrig, & + varid_DepthNew, & + varid_maxLevCellOrig, & + varid_maxLevCellNew, & + varid_RestThickOrig, & + varid_RestThickNew + + integer :: & + status + + integer :: & + dimid_nCells, & + dimid_nVertLevels, & + dimid_ni_a, & + dimid_nj_a, & + dimid_nv_a, & + dimid_src_grid_rank, & + dimid_n_b, & + dimid_ni_b, & + dimid_nj_b, & + dimid_nv_b, & + dimid_dst_grid_rank, & + dimid_n_s + + integer :: & + varid_xc_a, & + varid_yc_a, & + varid_xv_a, & + varid_yv_a, & + varid_mask_a, & + varid_area_a, & + varid_frac_a, & + varid_src_grid_dims, & + varid_xc_b, & + varid_yc_b, & + varid_xv_b, & + varid_yv_b, & + varid_mask_b, & + varid_area_b, & + varid_frac_b, & + varid_dst_grid_dims + + character(len=1000) :: & + date, & + time, & + zone, & + datetime + + integer, dimension(1) :: & + start1D, & + count1D + + integer, dimension(2) :: & + start2D, & + count2D + + write(*,*) "Create depth file...", trim(filename) + + ! create +! status = nf90_create(trim(filename), NF90_CLOBBER, ncid) + status = nf90_create(trim(filename), NF90_64BIT_OFFSET, ncid) + call netcdf_error(status, "create_depth_file: nf90_open") + + ! define dimensions + ! nCells + status = nf90_def_dim(ncid, "nCells", nCells, dimid_nCells) + call netcdf_error(status, "create_depth_file: nf90_def_dim nCells") + + ! nVertLevels + status = nf90_def_dim(ncid, "nVertLevels", nVertLevels, dimid_nVertLevels) + call netcdf_error(status, "create_depth_file: nf90_def_dim nVertLevels") + + ! define variables + ! bottomDepthOrig + status = nf90_def_var(ncid, "bottomDepthOrig", NF90_DOUBLE, (/dimid_nCells/), varid_DepthOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var bottomDepthOrig") + + status = nf90_put_att(ncid, varid_DepthOrig, "long_name", & + "Depth of the bottom of the ocean. Given as a positive distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att DepthOrig") + + status = nf90_put_att(ncid, varid_DepthOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att DepthOrig") + + ! bottomDepthNew + status = nf90_def_var(ncid, "bottomDepthNew", NF90_DOUBLE, (/dimid_nCells/), varid_DepthNew) + call netcdf_error(status, "create_depth_file: nf90_def_var bottomDepthNew") + + status = nf90_put_att(ncid, varid_DepthNew, "long_name", & + "Depth of the bottom of the ocean. Given as a positive distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att DepthNew") + + status = nf90_put_att(ncid, varid_DepthNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att DepthNew") + + ! maxLevelCellOrig + status = nf90_def_var(ncid, "maxLevelCellOrig", NF90_INT, (/dimid_nCells/), varid_maxLevCellOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var maxLevelCellOrig") + + status = nf90_put_att(ncid, varid_maxLevCellOrig, "long_name", & + "Index to the last active ocean cell in each column.") + call netcdf_error(status, "create_depth_file: nf90_put_att maxLevelCellOrig") + + status = nf90_put_att(ncid, varid_maxLevCellOrig, "units", "unitless") + call netcdf_error(status, "create_depth_file: nf90_put_att maxLevelCellOrig") + + ! maxLevelCellNew + status = nf90_def_var(ncid, "maxLevelCellNew", NF90_INT, (/dimid_nCells/), varid_maxLevCellNew) + call netcdf_error(status, "create_depth_file: nf90_def_var maxLevelCellNew") + + status = nf90_put_att(ncid, varid_maxLevCellNew, "long_name", & + "Index to the last active ocean cell in each column.") + call netcdf_error(status, "create_depth_file: nf90_put_att maxLevelCellNew") + + status = nf90_put_att(ncid, varid_maxLevCellNew, "units", "unitless") + call netcdf_error(status, "create_depth_file: nf90_put_att maxLevelCellOrig") + + ! restingThicknessOrig + status = nf90_def_var(ncid, "restingThicknessOrig", NF90_DOUBLE, (/dimid_nVertLevels,dimid_nCells/), varid_RestThickOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var restingThicknessOrig") + + status = nf90_put_att(ncid, varid_RestThickOrig, "long_name", & + "Layer thickness when the ocean is at rest, i.e. without SSH or internal perturbations.") + call netcdf_error(status, "create_depth_file: nf90_put_att RestThickOrig") + + status = nf90_put_att(ncid, varid_RestThickOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att RestThickOrig") + + ! restingThicknessNew + status = nf90_def_var(ncid, "restingThicknessNew", NF90_DOUBLE, (/dimid_nVertLevels,dimid_nCells/), varid_RestThickNew) + call netcdf_error(status, "create_depth_file: nf90_def_var restingThicknessNew") + + status = nf90_put_att(ncid, varid_RestThickNew, "long_name", & + "Layer thickness when the ocean is at rest, i.e. without SSH or internal perturbations.") + call netcdf_error(status, "create_depth_file: nf90_put_att RestThickNew") + + status = nf90_put_att(ncid, varid_RestThickNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att RestThickNew") + + ! global attributes + status = nf90_put_att(ncid, NF90_GLOBAL, "input_depth_file", trim(filename_depth_in)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_depth_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "input_mesh_file", trim(filename_mpas_mesh)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_mesh_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "smoothing_method", "2D Gaussian smoothing") + call netcdf_error(status, "create_depth_file: nf90_put_att smoothing_method") + + status = nf90_put_att(ncid, NF90_GLOBAL, "created_by", "smooth_runoff.exe") + call netcdf_error(status, "create_depth_file: nf90_put_att created_by") + + call date_and_time(date, time, zone) + datetime = date(1:4)//"-"//date(5:6)//"-"//date(7:8)//"_"//time(1:2)//":"//time(3:4)//":"//time(5:6)//" "//trim(zone) + status = nf90_put_att(ncid, NF90_GLOBAL, "created_at", trim(datetime)) + call netcdf_error(status, "create_depth_file: nf90_put_att created_at") + + status = nf90_put_att(ncid, NF90_GLOBAL, "distanceLimit", distanceLimit) + call netcdf_error(status, "create_depth_file: nf90_put_att distanceLimit") + + status = nf90_put_att(ncid, NF90_GLOBAL, "stdDeviation", stdDeviation) + call netcdf_error(status, "create_depth_file: nf90_put_att stdDeviation") + + ! end definition phase + status = nf90_enddef(ncid) + call netcdf_error(status, "create_depth_file: nf90_enddef") + + ! write variables + start1D(1) = 1 + count1D(1) = nCells + + ! bottomDepthOrig + status = nf90_put_var(ncid, varid_DepthOrig, bottomDepthOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var DepthOrig") + + ! bottomDepthNew + status = nf90_put_var(ncid, varid_DepthNew, bottomDepthNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var DepthNew") + + ! maxLevelCellOrig + status = nf90_put_var(ncid, varid_maxLevCellOrig, maxLevelCellOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var maxLevelCellOrig") + + ! maxLevelCellNew + status = nf90_put_var(ncid, varid_maxLevCellNew, maxLevelCellNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var maxLevelCellNew") + + start2D(1) = 1 + start2D(2) = 1 + count2D(2) = nCells + count2D(1) = nVertLevels + + ! restingThicknessOrig + status = nf90_put_var(ncid, varid_RestThickOrig, restingThicknessOrig, start2D, count2D) + call netcdf_error(status, "create_depth_file: nf90_put_var restingThicknessOrig") + + ! restingThicknessNew + status = nf90_put_var(ncid, varid_RestThickNew, restingThicknessNew, start2D, count2D) + call netcdf_error(status, "create_depth_file: nf90_put_var restingThicknessNew") + + end subroutine create_depth_file + + !---------------------------------------------------------------- + + subroutine close_depth_file(ncid) + + integer, intent(in) :: & + ncid + + integer :: & + status + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "close_depth_file: nf90_close ") + + end subroutine close_depth_file + + !---------------------------------------------------------------- + + subroutine load_mesh_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load mesh file..." + + ! open file + status = nf90_open(trim(filename_mpas_mesh), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_mesh_file: nf90_open") + + ! nCells + status = nf90_inq_dimid(ncid, "nCells", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nCells") + + status = nf90_inquire_dimension(ncid, dimid, len=nCells) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nCells") + + ! nVertLevels + status = nf90_inq_dimid(ncid, "nVertLevels", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nVertLevels") + + status = nf90_inquire_dimension(ncid, dimid, len=nVertLevels) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nVertLevels") + + ! nEdges + status = nf90_inq_dimid(ncid, "nEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=nEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nEdges") + + ! maxEdges + status = nf90_inq_dimid(ncid, "maxEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid maxEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=maxEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension maxEdges") + + allocate(xCell(nCells)) + allocate(yCell(nCells)) + allocate(zCell(nCells)) + allocate(latCell(nCells)) + allocate(lonCell(nCells)) + allocate(areaCell(nCells)) + allocate(dcEdge(nEdges)) + allocate(nEdgesOnCell(nCells)) + allocate(cellsOnCell(maxEdges,nCells)) + allocate(edgesOnCell(maxEdges,nCells)) + allocate(bottomDepthOrig(nCells)) + allocate(bottomDepthNew(nCells)) + allocate(bottomDepthPrev(nCells)) + allocate(maxLevelCellOrig(nCells)) + allocate(maxLevelCellNew(nCells)) + allocate(restingThicknessOrig(nVertLevels,nCells)) + allocate(restingThicknessNew(nVertLevels,nCells)) + allocate(landIceMask(nCells)) + + ! xCell + status = nf90_inq_varid(ncid, "xCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid xCell") + + status = nf90_get_var(ncid, varid, xCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var xCell") + + ! yCell + status = nf90_inq_varid(ncid, "yCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid yCell") + + status = nf90_get_var(ncid, varid, yCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var yCell") + + ! zCell + status = nf90_inq_varid(ncid, "zCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid zCell") + + status = nf90_get_var(ncid, varid, zCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var zCell") + + ! latCell + status = nf90_inq_varid(ncid, "latCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid latCell") + + status = nf90_get_var(ncid, varid, latCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var latCell") + + ! lonCell + status = nf90_inq_varid(ncid, "lonCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid lonCell") + + status = nf90_get_var(ncid, varid, lonCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var lonCell") + + ! areaCell + status = nf90_inq_varid(ncid, "areaCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid areaCell") + + status = nf90_get_var(ncid, varid, areaCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var areaCell") + + ! dcEdge + status = nf90_inq_varid(ncid, "dcEdge", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid dcEdge") + + status = nf90_get_var(ncid, varid, dcEdge) + call netcdf_error(status, "load_mesh_file: nf90_get_var dcEdge") + + ! nEdgesOnCell + status = nf90_inq_varid(ncid, "nEdgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid nEdgesOnCell") + + status = nf90_get_var(ncid, varid, nEdgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var nEdgesOnCell") + + ! cellsOnCell + status = nf90_inq_varid(ncid, "cellsOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid cellsOnCell") + + status = nf90_get_var(ncid, varid, cellsOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var cellsOnCell") + + ! edgesOnCell + status = nf90_inq_varid(ncid, "edgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid edgesOnCell") + + status = nf90_get_var(ncid, varid, edgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var edgesOnCell") + + ! bottomDepth + status = nf90_inq_varid(ncid, "bottomDepth", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid bottomDepth") + + status = nf90_get_var(ncid, varid, bottomDepthOrig) + call netcdf_error(status, "load_mesh_file: nf90_get_var bottomDepth") + + ! maxLevelCell + status = nf90_inq_varid(ncid, "maxLevelCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid maxLevelCell") + + status = nf90_get_var(ncid, varid, maxLevelCellOrig) + call netcdf_error(status, "load_mesh_file: nf90_get_var maxLevelCell") + + ! restingThickness + status = nf90_inq_varid(ncid, "restingThickness", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid restingThickness") + + status = nf90_get_var(ncid, varid, restingThicknessOrig) + call netcdf_error(status, "load_mesh_file: nf90_get_var restingThickness") + + ! landIceMask + status = nf90_inq_varid(ncid, "landIceMask", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid landIceMask") + + status = nf90_get_var(ncid, varid, landIceMask) + call netcdf_error(status, "load_mesh_file: nf90_get_var landIceMask") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_mesh_file: nf90_close") + + end subroutine load_mesh_file + + !---------------------------------------------------------------- + + subroutine netcdf_error(status, message) + + integer, intent(in) :: & + status + + character(len=*), intent(in) :: & + message + + if (status /= 0) then + write(*,*) "Netcdf error: ", status, nf90_strerror(status) + write(*,*) trim(message) + stop + endif + + end subroutine netcdf_error + + !---------------------------------------------------------------- + +end program smooth_topography From ef25978af7a06a953eb233ce68b372a6daf832d2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 18 Nov 2023 17:20:18 -0700 Subject: [PATCH 052/169] Add a tool for smoothing ocean topo before init --- ocean/smooth_topo/CMakeLists.txt | 6 +- ocean/smooth_topo/README | 21 +- ocean/smooth_topo/smooth_topo_before_init.F90 | 759 ++++++++++++++++++ 3 files changed, 780 insertions(+), 6 deletions(-) create mode 100644 ocean/smooth_topo/smooth_topo_before_init.F90 diff --git a/ocean/smooth_topo/CMakeLists.txt b/ocean/smooth_topo/CMakeLists.txt index a06d7a180..0026640de 100644 --- a/ocean/smooth_topo/CMakeLists.txt +++ b/ocean/smooth_topo/CMakeLists.txt @@ -11,7 +11,11 @@ message (STATUS "NETCDF_INCLUDES=${NETCDF_INCLUDES}") message (STATUS "NETCDF_LIBRARIES=${NETCDF_LIBRARIES}") include_directories(${NETCDF_INCLUDES}) + add_executable (ocean_smooth_topo_skip_land_ice smooth_topo_skip_land_ice.F90) target_link_libraries (ocean_smooth_topo_skip_land_ice ${NETCDF_LIBRARIES}) -install (TARGETS ocean_smooth_topo_skip_land_ice DESTINATION bin) +add_executable (ocean_smooth_topo_before_init smooth_topo_before_init.F90) +target_link_libraries (ocean_smooth_topo_before_init ${NETCDF_LIBRARIES}) + +install (TARGETS ocean_smooth_topo_skip_land_ice ocean_smooth_topo_before_init DESTINATION bin) diff --git a/ocean/smooth_topo/README b/ocean/smooth_topo/README index 19384f482..990096f33 100644 --- a/ocean/smooth_topo/README +++ b/ocean/smooth_topo/README @@ -1,10 +1,18 @@ Authors: Adrian Turner, Mat Maltrud -A tool for smoothing bathymetry in an MPAS-Ocean initial condition with a -Gaussian filter with a characteristic length scale (stdDeviation) and cut-off -distance (distanceLimit). The smoothing is only applied if the original -maxLevelCell is larger than a given threshold (minLevelForSmoothing). Smoothing -is applied over a given number of iterations (numSmoothingPasses). +Tools for smoothing bathymetry in an MPAS-Ocean with a Gaussian filter with a +characteristic length scale (stdDeviation) and cut-off distance +(distanceLimit). Smoothing is applied over a given number of iterations +(numSmoothingPasses). The tools do not currently work on meshes without +ice-shelf cavities. + +The smooth_topo_skip_land_ice tool can be applied to topography after an +initial condition has been created. The smoothing is only applied if the +original maxLevelCell is deeper than a given threshold (minLevelForSmoothing) +and ice-shelf cavities are ignored. + +The smooth_topo_before_init tool is used to smooth topography data on the MPAS +mesh before an initial condition has been created. An example namelist file (smooth_depth_in) is: @@ -17,3 +25,6 @@ An example namelist file (smooth_depth_in) is: numSmoothingPasses = 1 minLevelForSmoothing = 5 / + +The minLevelForSmoothing namelist option only applies to +smooth_topo_skip_land_ice. diff --git a/ocean/smooth_topo/smooth_topo_before_init.F90 b/ocean/smooth_topo/smooth_topo_before_init.F90 new file mode 100644 index 000000000..0227548f8 --- /dev/null +++ b/ocean/smooth_topo/smooth_topo_before_init.F90 @@ -0,0 +1,759 @@ + +program smooth_topography + +! on compy +! module load gcc;module load netcdf +!gfortran smooth_topo_before_init.F90 -o a.out -I /share/apps/netcdf/4.6.3/gcc/4.8.5/include/ -L /share/apps/netcdf/4.6.3/gcc/4.8.5/lib/ -lnetcdf -lnetcdff + + use netcdf + + implicit none + + integer, parameter :: & + RKIND = selected_real_kind(13) + + ! input parameters + character(len=1000) :: & + filename_depth_in, & + filename_depth_out, & + filename_mpas_mesh, & + filename_diagnostic + + real(kind=RKIND) :: & + layerBottom, dz, & + layerTop, partialCell_dz, & + maxBottomThicknessOrig, & + distanceLimit, & ! (km) + stdDeviation ! (km) + + integer :: & + numSmoothingPasses, smoothingPass, minLevelForSmoothing + + namelist /smooth/ & + filename_depth_in, & + filename_depth_out, & + filename_mpas_mesh, & + distanceLimit, & + stdDeviation, & + numSmoothingPasses + + ! mpas grid + integer :: & + nCells, & + nEdges, & + iCellFull, & + maxEdges + + real(kind=RKIND), dimension(:), allocatable :: & + xCell, & + yCell, & + zCell, & + lonCell, & + latCell, & + areaCell, & + bedElevationOrig, & + bedElevationNew, & + bedElevationPrev, & + landIceDraftOrig, & + landIceDraftNew, & + landIceDraftPrev, & + landIceThkOrig, & + landIceThkNew, & + landIceThkPrev, & + dcEdge + + integer, dimension(:), allocatable :: & + nEdgesOnCell + + integer, dimension(:,:), allocatable :: & + cellsOnCell, & + edgesOnCell + + ! temporary variables + integer :: & + is, & + ia, & + iCell, & + k, & + start, & + count, & + ncid, & + varid_BedOrig, & + varid_BedNew, & + varid_IceDraftOrig, & + varid_IceDraftNew, & + varid_IceThkOrig, & + varid_IceThkNew + + real(kind=RKIND), dimension(:), allocatable :: & + weights, & + cellDistance, & + distanceSpread + + integer, dimension(:), allocatable :: & + iCellsSpread, & + cellStatus + + real(kind=RKIND), parameter :: & + km_to_m = 1000.0_RKIND + + ! diagnostic variables + real(kind=RKIND), dimension(:), allocatable :: & + weightsDiagnostic + + ! read in namelist + write(*,*) "Reading namelist..." + open(11,file="smooth_depth_in",status='old') + read(11,nml=smooth) + close(11) + + ! load the MPAS mesh file + call load_mesh_file() + + call load_topo_file() + + ! allocate temporary arrays + write(*,*) "Allocate temporary variables..." + allocate(weights(nCells)) + allocate(cellDistance(nCells)) + allocate(distanceSpread(nCells)) + allocate(iCellsSpread(nCells)) + allocate(cellStatus(nCells)) + + write(*,*) "Perform smoothing..." + + bedElevationPrev = bedElevationOrig + bedElevationNew = bedElevationOrig ! for cells that do not get smoothed + + landIceDraftPrev = landIceDraftOrig + landIceDraftNew = landIceDraftOrig ! for cells that do not get smoothed + + landIceThkPrev = landIceThkOrig + landIceThkNew = landIceThkOrig ! for cells that do not get smoothed + +! convert to degrees + latCell = latCell*180./3.14159 + lonCell = lonCell*180./3.14159 + + do smoothingPass = 1, numSmoothingPasses + + write(*,*)' smoothing pass ',smoothingPass + + ! perform the smoothing + do iCell = 1, nCells + + call smooth_topo(& + iCell, & + distanceLimit * km_to_m, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceSpread) + enddo ! iCell + + bedElevationPrev = bedElevationNew + landIceDraftPrev = landIceDraftNew + landIceThkPrev = landIceThkNew + enddo ! smoothingPass + + ! output the depth file + call create_depth_file(filename_depth_out, ncid, & + varid_BedOrig, varid_BedNew, varid_IceDraftOrig, varid_IceDraftNew, & + varid_IceThkOrig, varid_IceThkNew) + + ! close the depth file + call close_depth_file(ncid) + +contains + + !---------------------------------------------------------------- + + function gaussian_weight(distance, stdDeviation) result(weight) + + real(kind=RKIND), intent(in) :: & + distance, & + stdDeviation + + real(kind=RKIND) :: weight + + weight = exp(-1.0_RKIND * (distance**2 / (2.0_RKIND * stdDeviation**2))) + + end function gaussian_weight + + !---------------------------------------------------------------- + + subroutine smooth_topo(& + iCell_Start, & + distanceLimit, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceOut) + + integer, intent(in) :: & + iCell_Start + + real(kind=RKIND), intent(in) :: & + distanceLimit + + integer, dimension(:), intent(in) :: & + nEdgesOnCell + + integer, dimension(:,:), intent(in) :: & + cellsOnCell, & + edgesOnCell + + real(kind=RKIND), dimension(:), intent(in) :: & + dcEdge + + integer, dimension(:), intent(out) :: & + cellStatus + + real(kind=RKIND), dimension(:), intent(out) :: & + cellDistance, & + distanceOut + + integer :: & + nCellsOut + + integer, dimension(:), allocatable :: & + iCellsPrev, & + iCellsOut, & + iCellsNext + + integer :: & + nCellsPrev, & + nCellsNext, & + nLayer, & + iCellPrev, & + iCellOnCell, & + iCellNext, & + iEdge + + real(kind=RKIND) :: & + cellDistanceToAdd, & + weight, & + smoothedBedSum, & + smoothedIceDraftSum, & + smoothedIceThkSum, & + weightSum + + allocate(iCellsPrev(nCells)) + allocate(iCellsNext(nCells)) + allocate(iCellsOut(nCells)) + + ! set the internal variables + cellStatus = -1 + cellDistance = -1.0_RKIND + + ! set first cell + cellStatus(iCell_Start) = 0 + nCellsPrev = 1 + iCellsPrev(1) = iCell_Start + + ! first output cell is the starting cell + nCellsOut = 1 + iCellsOut(1) = iCell_Start + distanceOut(1) = 0.0_RKIND + + ! initialize sums + smoothedBedSum = 0.0_RKIND + smoothedIceDraftSum = 0.0_RKIND + smoothedIceThkSum = 0.0_RKIND + weightSum = 0.0_RKIND + + ! loop over cell layers from original cell + nLayer = 0 + do while (nCellsPrev > 0) + + ! reset number of cells found this layer + nCellsNext = 0 + + ! loop over cells defined in the previous iteration + do iCellPrev = 1, nCellsPrev + + ! loop over neighbours of these previous cells + do iCellOnCell = 1, nEdgesOnCell(iCellsPrev(iCellPrev)) + + ! get the iCell of the next potential cell in the next cells + iCellNext = cellsOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + ! get the edge index for this crossing + iEdge = edgesOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + cellDistanceToAdd = cellDistance(iCellsPrev(iCellPrev)) + dcEdge(iEdge) + + ! check to see if we need to add it to the next array + if (cellStatus(iCellNext) == -1 .and. cellDistanceToAdd < distanceLimit) then + + ! count how many on the next list + nCellsNext = nCellsNext + 1 + + ! add this new cell to the next list + iCellsNext(nCellsNext) = iCellNext + + ! update the status of the cell + cellStatus(iCellNext) = nLayer + + ! calculate the distance to this cell + cellDistance(iCellNext) = cellDistanceToAdd + + ! output + nCellsOut = nCellsOut + 1 + iCellsOut(nCellsOut) = iCellNext + distanceOut(nCellsOut) = cellDistanceToAdd + + weight = gaussian_weight(cellDistanceToAdd, stdDeviation * km_to_m) + weightSum = weightSum + weight + smoothedBedSum = smoothedBedSum + weight*bedElevationPrev(iCellNext) + smoothedIceDraftSum = smoothedIceDraftSum + weight*landIceDraftPrev(iCellNext) + smoothedIceThkSum = smoothedIceThkSum + weight*landIceThkPrev(iCellNext) + + endif ! cellStatus(iCellNext) == -1 + + enddo ! iCellOnCell + + enddo ! iCellPrev + + ! swap next and prev + nCellsPrev = nCellsNext + + iCellsPrev(1:nCellsNext) = iCellsnext(1:nCellsNext) + + ! increment the layer number + nLayer = nLayer + 1 + + enddo ! nCellsNext > 0 + + bedElevationNew(iCell_Start) = smoothedBedSum/weightSum + landIceDraftNew(iCell_Start) = smoothedIceDraftSum/weightSum + landIceThkNew(iCell_Start) = smoothedIceThkSum/weightSum + + deallocate(iCellsPrev) + deallocate(iCellsNext) + + end subroutine smooth_topo + + !---------------------------------------------------------------- + + subroutine create_depth_file(filename, ncid, & + varid_BedOrig, varid_BedNew, varid_IceDraftOrig, varid_IceDraftNew, & + varid_IceThkOrig, varid_IceThkNew) + + character(len=*), intent(in) :: & + filename + + integer, intent(out) :: & + ncid, & + varid_BedOrig, & + varid_BedNew, & + varid_IceDraftOrig, & + varid_IceDraftNew, & + varid_IceThkOrig, & + varid_IceThkNew + + integer :: & + status + + integer :: & + dimid_nCells, & + dimid_ni_a, & + dimid_nj_a, & + dimid_nv_a, & + dimid_src_grid_rank, & + dimid_n_b, & + dimid_ni_b, & + dimid_nj_b, & + dimid_nv_b, & + dimid_dst_grid_rank, & + dimid_n_s + + integer :: & + varid_xc_a, & + varid_yc_a, & + varid_xv_a, & + varid_yv_a, & + varid_mask_a, & + varid_area_a, & + varid_frac_a, & + varid_src_grid_dims, & + varid_xc_b, & + varid_yc_b, & + varid_xv_b, & + varid_yv_b, & + varid_mask_b, & + varid_area_b, & + varid_frac_b, & + varid_dst_grid_dims + + character(len=1000) :: & + date, & + time, & + zone, & + datetime + + integer, dimension(1) :: & + start1D, & + count1D + + integer, dimension(2) :: & + start2D, & + count2D + + write(*,*) "Create depth file...", trim(filename) + + ! create +! status = nf90_create(trim(filename), NF90_CLOBBER, ncid) + status = nf90_create(trim(filename), NF90_64BIT_OFFSET, ncid) + call netcdf_error(status, "create_depth_file: nf90_open") + + ! define dimensions + ! nCells + status = nf90_def_dim(ncid, "nCells", nCells, dimid_nCells) + call netcdf_error(status, "create_depth_file: nf90_def_dim nCells") + + ! define variables + ! bedElevationOrig + status = nf90_def_var(ncid, "bed_elevationOrig", NF90_DOUBLE, (/dimid_nCells/), varid_BedOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var bedElevationOrig") + + status = nf90_put_att(ncid, varid_BedOrig, "long_name", & + "Elevation of the bottom of the ocean. Given as a negative distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationOrig") + + status = nf90_put_att(ncid, varid_BedOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationOrig") + + ! bedElevationNew + status = nf90_def_var(ncid, "bed_elevationNew", NF90_DOUBLE, (/dimid_nCells/), varid_BedNew) + call netcdf_error(status, "create_depth_file: nf90_def_var bedElevationNew") + + status = nf90_put_att(ncid, varid_BedNew, "long_name", & + "Elevation of the bottom of the ocean. Given as a negative distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationNew") + + status = nf90_put_att(ncid, varid_BedNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationNew") + + ! landIceDraftOrig + status = nf90_def_var(ncid, "landIceDraftObservedOrig", NF90_DOUBLE, (/dimid_nCells/), varid_IceDraftOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var landIceDraftOrig") + + status = nf90_put_att(ncid, varid_IceDraftOrig, "long_name", & + "The elevation of the bottom of land ice.") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceDraftOrig") + + status = nf90_put_att(ncid, varid_IceDraftOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceDraftOrig") + + ! landIceDraftNew + status = nf90_def_var(ncid, "landIceDraftObservedNew", NF90_DOUBLE, (/dimid_nCells/), varid_IceDraftNew) + call netcdf_error(status, "create_depth_file: nf90_def_var landIceDraftNew") + + status = nf90_put_att(ncid, varid_IceDraftNew, "long_name", & + "The elevation of the bottom of land ice.") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceDraftNew") + + status = nf90_put_att(ncid, varid_IceDraftNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceDraftNew") + + ! landIceThkOrig + status = nf90_def_var(ncid, "landIceThkObservedOrig", NF90_DOUBLE, (/dimid_nCells/), varid_IceThkOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var landIceThkOrig") + + status = nf90_put_att(ncid, varid_IceThkOrig, "long_name", & + "The thickness of the land ice.") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceThkOrig") + + status = nf90_put_att(ncid, varid_IceThkOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceThkOrig") + + ! landIceThkNew + status = nf90_def_var(ncid, "landIceThkObservedNew", NF90_DOUBLE, (/dimid_nCells/), varid_IceThkNew) + call netcdf_error(status, "create_depth_file: nf90_def_var landIceThkNew") + + status = nf90_put_att(ncid, varid_IceThkNew, "long_name", & + "The thickness of the land ice.") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceThkNew") + + status = nf90_put_att(ncid, varid_IceThkNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att landIceThkNew") + + ! global attributes + status = nf90_put_att(ncid, NF90_GLOBAL, "input_depth_file", trim(filename_depth_in)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_depth_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "input_mesh_file", trim(filename_mpas_mesh)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_mesh_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "smoothing_method", "2D Gaussian smoothing") + call netcdf_error(status, "create_depth_file: nf90_put_att smoothing_method") + + status = nf90_put_att(ncid, NF90_GLOBAL, "created_by", "smooth_topo_before_init") + call netcdf_error(status, "create_depth_file: nf90_put_att created_by") + + call date_and_time(date, time, zone) + datetime = date(1:4)//"-"//date(5:6)//"-"//date(7:8)//"_"//time(1:2)//":"//time(3:4)//":"//time(5:6)//" "//trim(zone) + status = nf90_put_att(ncid, NF90_GLOBAL, "created_at", trim(datetime)) + call netcdf_error(status, "create_depth_file: nf90_put_att created_at") + + status = nf90_put_att(ncid, NF90_GLOBAL, "distanceLimit", distanceLimit) + call netcdf_error(status, "create_depth_file: nf90_put_att distanceLimit") + + status = nf90_put_att(ncid, NF90_GLOBAL, "stdDeviation", stdDeviation) + call netcdf_error(status, "create_depth_file: nf90_put_att stdDeviation") + + ! end definition phase + status = nf90_enddef(ncid) + call netcdf_error(status, "create_depth_file: nf90_enddef") + + ! write variables + start1D(1) = 1 + count1D(1) = nCells + + ! bedElevationOrig + status = nf90_put_var(ncid, varid_BedOrig, bedElevationOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var bedElevationOrig") + + ! bedElevationNew + status = nf90_put_var(ncid, varid_BedNew, bedElevationNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var bedElevationNew") + + ! landIceDraftOrig + status = nf90_put_var(ncid, varid_IceDraftOrig, landIceDraftOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var landIceDraftOrig") + + ! landIceDraftNew + status = nf90_put_var(ncid, varid_IceDraftNew, landIceDraftNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var landIceDraftNew") + + ! landIceThkOrig + status = nf90_put_var(ncid, varid_IceThkOrig, landIceThkOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var landIceThkOrig") + + ! landIceThkNew + status = nf90_put_var(ncid, varid_IceThkNew, landIceThkNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var landIceThkNew") + + end subroutine create_depth_file + + !---------------------------------------------------------------- + + subroutine close_depth_file(ncid) + + integer, intent(in) :: & + ncid + + integer :: & + status + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "close_depth_file: nf90_close ") + + end subroutine close_depth_file + + !---------------------------------------------------------------- + + subroutine load_mesh_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load mesh file..." + + ! open file + status = nf90_open(trim(filename_mpas_mesh), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_mesh_file: nf90_open") + + ! nCells + status = nf90_inq_dimid(ncid, "nCells", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nCells") + + status = nf90_inquire_dimension(ncid, dimid, len=nCells) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nCells") + + ! nEdges + status = nf90_inq_dimid(ncid, "nEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=nEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nEdges") + + ! maxEdges + status = nf90_inq_dimid(ncid, "maxEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid maxEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=maxEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension maxEdges") + + allocate(xCell(nCells)) + allocate(yCell(nCells)) + allocate(zCell(nCells)) + allocate(latCell(nCells)) + allocate(lonCell(nCells)) + allocate(areaCell(nCells)) + allocate(dcEdge(nEdges)) + allocate(nEdgesOnCell(nCells)) + allocate(cellsOnCell(maxEdges,nCells)) + allocate(edgesOnCell(maxEdges,nCells)) + + ! xCell + status = nf90_inq_varid(ncid, "xCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid xCell") + + status = nf90_get_var(ncid, varid, xCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var xCell") + + ! yCell + status = nf90_inq_varid(ncid, "yCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid yCell") + + status = nf90_get_var(ncid, varid, yCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var yCell") + + ! zCell + status = nf90_inq_varid(ncid, "zCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid zCell") + + status = nf90_get_var(ncid, varid, zCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var zCell") + + ! latCell + status = nf90_inq_varid(ncid, "latCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid latCell") + + status = nf90_get_var(ncid, varid, latCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var latCell") + + ! lonCell + status = nf90_inq_varid(ncid, "lonCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid lonCell") + + status = nf90_get_var(ncid, varid, lonCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var lonCell") + + ! areaCell + status = nf90_inq_varid(ncid, "areaCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid areaCell") + + status = nf90_get_var(ncid, varid, areaCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var areaCell") + + ! dcEdge + status = nf90_inq_varid(ncid, "dcEdge", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid dcEdge") + + status = nf90_get_var(ncid, varid, dcEdge) + call netcdf_error(status, "load_mesh_file: nf90_get_var dcEdge") + + ! nEdgesOnCell + status = nf90_inq_varid(ncid, "nEdgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid nEdgesOnCell") + + status = nf90_get_var(ncid, varid, nEdgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var nEdgesOnCell") + + ! cellsOnCell + status = nf90_inq_varid(ncid, "cellsOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid cellsOnCell") + + status = nf90_get_var(ncid, varid, cellsOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var cellsOnCell") + + ! edgesOnCell + status = nf90_inq_varid(ncid, "edgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid edgesOnCell") + + status = nf90_get_var(ncid, varid, edgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var edgesOnCell") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_mesh_file: nf90_close") + + end subroutine load_mesh_file + + !---------------------------------------------------------------- + + subroutine load_topo_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load topo file..." + + ! open file + status = nf90_open(trim(filename_depth_in), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_topo_file: nf90_open") + + allocate(bedElevationOrig(nCells)) + allocate(bedElevationNew(nCells)) + allocate(bedElevationPrev(nCells)) + allocate(landIceDraftOrig(nCells)) + allocate(landIceDraftNew(nCells)) + allocate(landIceDraftPrev(nCells)) + allocate(landIceThkOrig(nCells)) + allocate(landIceThkNew(nCells)) + allocate(landIceThkPrev(nCells)) + + ! bedElevation + status = nf90_inq_varid(ncid, "bed_elevation", varid) + call netcdf_error(status, "load_topo_file: nf90_inq_varid bed_elevation") + + status = nf90_get_var(ncid, varid, bedElevationOrig) + call netcdf_error(status, "load_topo_file: nf90_get_var bed_elevation") + + ! landIceDraft + status = nf90_inq_varid(ncid, "landIceDraftObserved", varid) + call netcdf_error(status, "load_topo_file: nf90_inq_varid landIceDraft") + + status = nf90_get_var(ncid, varid, landIceDraftOrig) + call netcdf_error(status, "load_topo_file: nf90_get_var landIceDraft") + + ! landIceThk + status = nf90_inq_varid(ncid, "landIceThkObserved", varid) + call netcdf_error(status, "load_topo_file: nf90_inq_varid landIceThk") + + status = nf90_get_var(ncid, varid, landIceThkOrig) + call netcdf_error(status, "load_topo_file: nf90_get_var landIceThk") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_topo_file: nf90_close") + + end subroutine load_topo_file + + !---------------------------------------------------------------- + + subroutine netcdf_error(status, message) + + integer, intent(in) :: & + status + + character(len=*), intent(in) :: & + message + + if (status /= 0) then + write(*,*) "Netcdf error: ", status, nf90_strerror(status) + write(*,*) trim(message) + stop + endif + + end subroutine netcdf_error + + !---------------------------------------------------------------- + +end program smooth_topography From 7dbda33d8f595c3c515070315c1dbaaccdfca87d Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 17 Nov 2023 16:36:31 -0600 Subject: [PATCH 053/169] Update to v0.28.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 2a8a6af02..e7c60d636 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -40,6 +40,7 @@ Documentation On GitHub `v0.25.0`_ `0.25.0`_ `v0.26.0`_ `0.26.0`_ `v0.27.0`_ `0.27.0`_ +`v0.28.0`_ `0.28.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -110,3 +111,5 @@ Documentation On GitHub .. _`0.26.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.26.0 .. _`v0.27.0`: ../0.27.0/index.html .. _`0.27.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.27.0 +.. _`v0.28.0`: ../0.28.0/index.html +.. _`0.28.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.28.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 2b9ef9c8e..15d81c7a1 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 27, 0) +__version_info__ = (0, 28, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 8aa8a884b..584571918 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.27.0" %} +{% set version = "0.28.0" %} package: name: {{ name|lower }} From b93e2ad8e8b367a9e7c93c1e1226ae83e442b62b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 17 Nov 2023 16:37:13 -0600 Subject: [PATCH 054/169] Update to hdf5 1.14.2 --- conda_package/ci/linux_python3.10.yaml | 2 +- conda_package/ci/linux_python3.11.yaml | 2 +- conda_package/ci/linux_python3.9.yaml | 2 +- conda_package/ci/osx_python3.10.yaml | 2 +- conda_package/ci/osx_python3.11.yaml | 2 +- conda_package/ci/osx_python3.9.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conda_package/ci/linux_python3.10.yaml b/conda_package/ci/linux_python3.10.yaml index 6dc07fca9..2fa257cee 100644 --- a/conda_package/ci/linux_python3.10.yaml +++ b/conda_package/ci/linux_python3.10.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/linux_python3.11.yaml b/conda_package/ci/linux_python3.11.yaml index aec419b8d..bb5761d64 100644 --- a/conda_package/ci/linux_python3.11.yaml +++ b/conda_package/ci/linux_python3.11.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/linux_python3.9.yaml b/conda_package/ci/linux_python3.9.yaml index 907732c15..03c939c7a 100644 --- a/conda_package/ci/linux_python3.9.yaml +++ b/conda_package/ci/linux_python3.9.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/osx_python3.10.yaml b/conda_package/ci/osx_python3.10.yaml index b6d14d6fb..37e52bc37 100644 --- a/conda_package/ci/osx_python3.10.yaml +++ b/conda_package/ci/osx_python3.10.yaml @@ -13,7 +13,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 llvm_openmp: diff --git a/conda_package/ci/osx_python3.11.yaml b/conda_package/ci/osx_python3.11.yaml index 20f434f2b..a1ce0cbb3 100644 --- a/conda_package/ci/osx_python3.11.yaml +++ b/conda_package/ci/osx_python3.11.yaml @@ -13,7 +13,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 llvm_openmp: diff --git a/conda_package/ci/osx_python3.9.yaml b/conda_package/ci/osx_python3.9.yaml index 965a2b9b7..7262ae4f0 100644 --- a/conda_package/ci/osx_python3.9.yaml +++ b/conda_package/ci/osx_python3.9.yaml @@ -13,7 +13,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.1 +- 1.14.2 libnetcdf: - 4.9.2 llvm_openmp: From 31442bc34fdb404b0b4b9f1c751c499907eac7be Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 22 Nov 2023 09:13:26 -0800 Subject: [PATCH 055/169] Add script to plot ice surfaces, bed topography, speed and temperature along a transect Add script to plot ice surfaces, bed topography, speed and optionally temperature along a transect. --- landice/output_processing_li/plot_transect.py | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 landice/output_processing_li/plot_transect.py diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py new file mode 100644 index 000000000..8b55110a0 --- /dev/null +++ b/landice/output_processing_li/plot_transect.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Plot bed and surface topography, surface speed, and +(optionally) temperature from MPAS netCDF along a transect. +@author: trevorhillebrand +""" + +import numpy as np +import csv +from netCDF4 import Dataset +from optparse import OptionParser +from scipy.interpolate import LinearNDInterpolator +import matplotlib.pyplot as plt +from matplotlib.pyplot import cm + +parser = OptionParser(description='Plot transect from MPAS netCDF') +parser.add_option("-d", "--data", dest="data_file", + help="the MPAS netCDF file") +parser.add_option("-c", "--coords", dest="coords_file", default=None, + help="csv file defining transect. x coordinates \ + in first column, y coordinates in second column. \ + No header.") +parser.add_option('-t', dest="times", default="-1", + help="integer time levels at which to plot \ + (int separated by commas; no spaces)") +parser.add_option('-x', dest='x_coords', default=None, + help='List of x coordinates of transect if not \ + using a csv file. Comma-separated, no spaces.') +parser.add_option('-y', dest='y_coords', default=None, + help='List of y coordinates of transect if not \ + using a csv file. Comma-separated, no spaces.') +parser.add_option("--temperature", dest="interp_temp", + action="store_true", help="interpolate temperature") + +for option in parser.option_list: + if option.default != ("NO", "DEFAULT"): + option.help += (" " if option.help else "") + "[default: %default]" +options, args = parser.parse_args() + +times_list = [i for i in options.times.split(',')] # list of string times for plotting +times = [int(i) for i in options.times.split(',')] # list of integer time indices + +dataset = Dataset(options.data_file, 'r') +dataset.set_always_mask(False) +xCell = dataset.variables["xCell"][:] +yCell = dataset.variables["yCell"][:] +nCells = dataset.dimensions['nCells'].size +areaCell = dataset.variables["areaCell"][:] +layerThicknessFractions = dataset.variables["layerThicknessFractions"][:] +nVertLevels = dataset.dimensions['nVertLevels'].size + +# replace -1 time index with last forward index of time array +times[times.index(-1)] = dataset.dimensions['Time'].size - 1 +times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) +# Cannot plot temperature for more than one time index. +if options.interp_temp and (len(times) > 1): + print('Cannot plot temperature for more than one time index.' + + ' Skipping temperature interpolation and plotting.') + options.interp_temp = False + +li_mask_ValueDynamicIce = 2 +cellMask = dataset.variables['cellMask'][:] +cellMask_dynamicIce = (cellMask & li_mask_ValueDynamicIce) // li_mask_ValueDynamicIce +# only take thickness of dynamic ice +thk = dataset.variables["thickness"][:] * cellMask_dynamicIce +# Include speed on non-dynamic ice to avoid interpolation artifacts. +if "surfaceSpeed" in dataset.variables.keys(): + speed = dataset.variables["surfaceSpeed"][:] * 3600. * 24. * 365. +else: + speed = np.sqrt(dataset.variables["uReconstructX"][:,:,0]**2. + + dataset.variables["uReconstructY"][:,:,0]**2.) + speed *= 3600. * 24. * 365. # convert from m/s to m/yr + +if options.interp_temp: + temperature = dataset.variables['temperature'][:] + +bedTopo = dataset.variables["bedTopography"][0,:] + +# Use coordinates from CSV file or -x -y options, but not both. +# CSV file takes precedent if both are present. +if options.coords_file is not None: + x = [] + y = [] + with open(options.coords_file, newline='') as csvfile: + reader = csv.reader(csvfile, delimiter=',') + + for row in reader: + x.append(float(row[0])) + y.append(float(row[1])) + if [options.x_coords, options.y_coords] is not [None, None]: + print('-c and -x/-y options were both provided. Reading from ', + f'{options.coords_file} and ignoring -x and -y settings.') +else: + x = [float(i) for i in options.x_coords.split(',')] + y = [float(i) for i in options.y_coords.split(',')] + +# increase sampling by a factor of 100 +xArray = np.interp(np.linspace(0, len(x)-1, 100*len(x)), + np.linspace(0, len(x)-1, len(x)), x) +yArray = np.interp(np.linspace(0, len(y)-1, 100*len(y)), + np.linspace(0, len(y)-1, len(y)), y) + +d_distance = np.zeros(len(xArray)) +for ii in np.arange(1, len(xArray)): + d_distance[ii] = np.sqrt( (xArray[ii] - xArray[ii-1])**2 + + (yArray[ii] - yArray[ii-1])**2 ) + +distance = np.cumsum(d_distance) / 1000. # in km for plotting + +transectFig, transectAx = plt.subplots(2,1, sharex=True, layout='constrained') +thickAx = transectAx[0] +thickAx.grid() +speedAx = transectAx[1] +speedAx.grid() +timeColors = cm.plasma(np.linspace(0,1,len(times))) + +plt.rcParams.update({'font.size': 16}) + +bed_interpolant = LinearNDInterpolator(np.vstack((xCell, yCell)).T, bedTopo) +bed_transect = bed_interpolant(np.vstack((xArray, yArray)).T) + +for i, time in enumerate(times): + thk_interpolant = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, thk[time,:]) + thk_transect = thk_interpolant(np.vstack((xArray, yArray)).T) + lower_surf = np.maximum( -910. / 1028. * thk_transect, bed_transect) + lower_surf_nan = lower_surf.copy() # for plotting + lower_surf_nan[thk_transect==0.] = np.nan + upper_surf = lower_surf + thk_transect + upper_surf_nan = upper_surf.copy() # for plotting + upper_surf_nan[thk_transect==0.] = np.nan + thickAx.plot(distance, lower_surf_nan, color=timeColors[i]) + thickAx.plot(distance, upper_surf_nan, color=timeColors[i]) + + speed_interpolant = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, speed[time,:]) + speed_transect = speed_interpolant(np.vstack((xArray, yArray)).T) + speed_transect[thk_transect == 0.] = np.nan + speedAx.plot(distance, speed_transect, color=timeColors[i]) + + if options.interp_temp: + layer_thk = np.zeros((len(thk_transect), nVertLevels + 1)) + layer_midpoints = np.zeros((len(thk_transect), nVertLevels)) + layer_interfaces = np.zeros((len(thk_transect), nVertLevels + 1)) + layer_thk[:,0] = 0. + for ii in range(len(thk_transect)): + layer_thk[ii,1:] = np.cumsum(layerThicknessFractions * + thk_transect[ii]) + layer_midpoints[i,:] = upper_surf[ii] - (layer_thk[ii,1:] + + layer_thk[ii,0:-1]) / 2. + layer_interfaces[ii,:] = upper_surf[ii] - layer_thk[ii,:] + + temp_transect = np.zeros((len(xArray), nVertLevels)) + for lev in range(nVertLevels): + print(f'Interpolating temperature for level {lev}') + temp_interpolant = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, + temperature[time,:,lev]) + temp_transect[:, lev] = temp_interpolant( + np.vstack((xArray, yArray)).T) + +thickAx.plot(distance, bed_transect, color='black') + +if options.interp_temp: + temp_transect[temp_transect == 0.] = np.nan + temp_plot = thickAx.pcolormesh( np.tile(distance, (nVertLevels+1,1)).T, + layer_interfaces[:,:], temp_transect[1:,:], + cmap='YlGnBu_r', + vmin=240., vmax=273.15) + +speedAx.set_xlabel('Distance (km)') +speedAx.set_ylabel('Surface\nspeed (m/yr)') +thickAx.set_ylabel('Elevation\n(m asl)') + +if options.interp_temp: + temp_cbar = plt.colorbar(temp_plot) + temp_cbar.set_label('Temperature (K)') + temp_cbar.ax.tick_params(labelsize=12) + +if len(times) > 1: + time_cbar = plt.colorbar(cm.ScalarMappable(cmap='plasma'), ax=speedAx) + time_cbar.set_label('time index') + time_cbar.set_ticks(times / np.max(times)) + time_cbar.set_ticklabels(times_list) + time_cbar.ax.tick_params(labelsize=12) + +plt.show() + +dataset.close() From bf63f6b21a935fd34a5a1bc9c6ed29bdcdc18915 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 22 Nov 2023 09:37:30 -0800 Subject: [PATCH 056/169] Fix small bug when -1 is not in requested time indices --- landice/output_processing_li/plot_transect.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 8b55110a0..6e690047d 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -51,8 +51,9 @@ nVertLevels = dataset.dimensions['nVertLevels'].size # replace -1 time index with last forward index of time array -times[times.index(-1)] = dataset.dimensions['Time'].size - 1 -times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) +if -1 in times: + times[times.index(-1)] = dataset.dimensions['Time'].size - 1 + times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) # Cannot plot temperature for more than one time index. if options.interp_temp and (len(times) > 1): print('Cannot plot temperature for more than one time index.' + From 0aef5b41a14834cf3e546379b93f997cbad83945 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 22 Nov 2023 09:53:05 -0800 Subject: [PATCH 057/169] Use daysSinceStart to define times, if present If daysSinceStart is present in the .nc file, use it to define the times for plotting. If not present, just use time indices as labels. --- landice/output_processing_li/plot_transect.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 6e690047d..05cf20c21 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -49,6 +49,12 @@ areaCell = dataset.variables["areaCell"][:] layerThicknessFractions = dataset.variables["layerThicknessFractions"][:] nVertLevels = dataset.dimensions['nVertLevels'].size +if "daysSinceStart" in dataset.variables.keys(): + use_yrs = True + yrs = dataset.variables["daysSinceStart"][:] / 365. + times_list = [str(yrs[i]) for i in times] +else: + use_yrs = False # replace -1 time index with last forward index of time array if -1 in times: @@ -179,12 +185,15 @@ temp_cbar.set_label('Temperature (K)') temp_cbar.ax.tick_params(labelsize=12) -if len(times) > 1: - time_cbar = plt.colorbar(cm.ScalarMappable(cmap='plasma'), ax=speedAx) - time_cbar.set_label('time index') +if (len(times) > 1): + time_cbar = plt.colorbar(cm.ScalarMappable(cmap='plasma'), ax=thickAx) + time_cbar.ax.tick_params(labelsize=12) + if use_yrs: + time_cbar.set_label('Year') + else: + time_cbar.set_label('time index') time_cbar.set_ticks(times / np.max(times)) time_cbar.set_ticklabels(times_list) - time_cbar.ax.tick_params(labelsize=12) plt.show() From aff4e900a4146da2c2824c8fc5bd6c450fb2c375 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 22 Nov 2023 10:22:55 -0800 Subject: [PATCH 058/169] Add a -s option to save png file Add a -s option to save png file. Also add further fix for -1 time index. --- landice/output_processing_li/plot_transect.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 05cf20c21..e7c2780a3 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -30,6 +30,9 @@ parser.add_option('-y', dest='y_coords', default=None, help='List of y coordinates of transect if not \ using a csv file. Comma-separated, no spaces.') +parser.add_option('-s', dest='save_filename', default=None, + help='Path to save .png to, if desired.') + parser.add_option("--temperature", dest="interp_temp", action="store_true", help="interpolate temperature") @@ -56,10 +59,18 @@ else: use_yrs = False -# replace -1 time index with last forward index of time array +# Replace -1 time index with last forward index of time array. +# It is unclear why these need to be in separate if-statements, but they do. if -1 in times: - times[times.index(-1)] = dataset.dimensions['Time'].size - 1 + times[times.index(-1)] = int(dataset.dimensions['Time'].size - 1) +if '-1' in times_list: times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) + +print(times_list) + +print(times) + +print(dataset.dimensions['Time'].size) # Cannot plot temperature for more than one time index. if options.interp_temp and (len(times) > 1): print('Cannot plot temperature for more than one time index.' + @@ -195,6 +206,9 @@ time_cbar.set_ticks(times / np.max(times)) time_cbar.set_ticklabels(times_list) +if options.save_filename is not None: + transectFig.savefig(options.save_filename, dpi=400, bbox_inches='tight') + plt.show() dataset.close() From 099bf72e598a9939240362ffeb510bf7b86ae9ad Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 12:32:03 -0800 Subject: [PATCH 059/169] Skip speed plot if surfaceSpeed and uReconstructX/Y are absent. --- landice/output_processing_li/plot_transect.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index e7c2780a3..ea9b79398 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -66,11 +66,6 @@ if '-1' in times_list: times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) -print(times_list) - -print(times) - -print(dataset.dimensions['Time'].size) # Cannot plot temperature for more than one time index. if options.interp_temp and (len(times) > 1): print('Cannot plot temperature for more than one time index.' + @@ -82,13 +77,20 @@ cellMask_dynamicIce = (cellMask & li_mask_ValueDynamicIce) // li_mask_ValueDynamicIce # only take thickness of dynamic ice thk = dataset.variables["thickness"][:] * cellMask_dynamicIce +plot_speed = True # Include speed on non-dynamic ice to avoid interpolation artifacts. if "surfaceSpeed" in dataset.variables.keys(): speed = dataset.variables["surfaceSpeed"][:] * 3600. * 24. * 365. +elif "surfaceSpeed" not in dataset.variables.keys() and \ + all([ii in dataset.variables.keys() for ii in ['uReconstructX', 'uReconstructY']]): + speed = np.sqrt(dataset.variables["uReconstructX"][:,:,0]**2. + + dataset.variables["uReconstructY"][:,:,0]**2.) + speed *= 3600. * 24. * 365. # convert from m/s to m/yr else: - speed = np.sqrt(dataset.variables["uReconstructX"][:,:,0]**2. + - dataset.variables["uReconstructY"][:,:,0]**2.) - speed *= 3600. * 24. * 365. # convert from m/s to m/yr + print('File does not contain surfaceSpeed or uReconstructX/Y.', + ' Skipping velocity plot.') + plot_speed = False + if options.interp_temp: temperature = dataset.variables['temperature'][:] @@ -150,12 +152,13 @@ upper_surf_nan[thk_transect==0.] = np.nan thickAx.plot(distance, lower_surf_nan, color=timeColors[i]) thickAx.plot(distance, upper_surf_nan, color=timeColors[i]) - - speed_interpolant = LinearNDInterpolator( - np.vstack((xCell, yCell)).T, speed[time,:]) - speed_transect = speed_interpolant(np.vstack((xArray, yArray)).T) - speed_transect[thk_transect == 0.] = np.nan - speedAx.plot(distance, speed_transect, color=timeColors[i]) + + if plot_speed: + speed_interpolant = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, speed[time,:]) + speed_transect = speed_interpolant(np.vstack((xArray, yArray)).T) + speed_transect[thk_transect == 0.] = np.nan + speedAx.plot(distance, speed_transect, color=timeColors[i]) if options.interp_temp: layer_thk = np.zeros((len(thk_transect), nVertLevels + 1)) From e9cf442415995d16d05058f122ba07f41ee9d38c Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 12:46:22 -0800 Subject: [PATCH 060/169] Print warning that bedTopography is only read at first time. --- landice/output_processing_li/plot_transect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index ea9b79398..262bb77a6 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -96,7 +96,8 @@ temperature = dataset.variables['temperature'][:] bedTopo = dataset.variables["bedTopography"][0,:] - +print('Reading bedTopography from the first time level only. If multiple', + 'times are needed, plot_transects.py will need to be updated.') # Use coordinates from CSV file or -x -y options, but not both. # CSV file takes precedent if both are present. if options.coords_file is not None: From 48afa276a32475bb58c8c3a7ad70799abfdd96c0 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 12:46:52 -0800 Subject: [PATCH 061/169] Increase sampling using dcEdge instead of using a hard-coded factor --- landice/output_processing_li/plot_transect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 262bb77a6..0807e4023 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -116,10 +116,12 @@ x = [float(i) for i in options.x_coords.split(',')] y = [float(i) for i in options.y_coords.split(',')] -# increase sampling by a factor of 100 -xArray = np.interp(np.linspace(0, len(x)-1, 100*len(x)), +# increase sampling to match highest mesh resolution +total_distance = np.sqrt( (x[0] - x[1])**2. + (y[0] - y[1])**2.) +n_samples = int(round(total_distance / np.min(dataset.variables["dcEdge"][:]))) +xArray = np.interp(np.linspace(0, len(x)-1, n_samples), np.linspace(0, len(x)-1, len(x)), x) -yArray = np.interp(np.linspace(0, len(y)-1, 100*len(y)), +yArray = np.interp(np.linspace(0, len(y)-1, n_samples), np.linspace(0, len(y)-1, len(y)), y) d_distance = np.zeros(len(xArray)) From 3056132fe741962818f6a4fe6e3d6b8b5c47f26b Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 13:37:35 -0800 Subject: [PATCH 062/169] Only read times that will be plotted --- landice/output_processing_li/plot_transect.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 0807e4023..ad9dc3016 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -73,18 +73,18 @@ options.interp_temp = False li_mask_ValueDynamicIce = 2 -cellMask = dataset.variables['cellMask'][:] +cellMask = dataset.variables['cellMask'][times,:] cellMask_dynamicIce = (cellMask & li_mask_ValueDynamicIce) // li_mask_ValueDynamicIce # only take thickness of dynamic ice -thk = dataset.variables["thickness"][:] * cellMask_dynamicIce +thk = dataset.variables["thickness"][times,:] * cellMask_dynamicIce plot_speed = True # Include speed on non-dynamic ice to avoid interpolation artifacts. if "surfaceSpeed" in dataset.variables.keys(): - speed = dataset.variables["surfaceSpeed"][:] * 3600. * 24. * 365. + speed = dataset.variables["surfaceSpeed"][times,:] * 3600. * 24. * 365. elif "surfaceSpeed" not in dataset.variables.keys() and \ all([ii in dataset.variables.keys() for ii in ['uReconstructX', 'uReconstructY']]): - speed = np.sqrt(dataset.variables["uReconstructX"][:,:,0]**2. + - dataset.variables["uReconstructY"][:,:,0]**2.) + speed = np.sqrt(dataset.variables["uReconstructX"][times,:,0]**2. + + dataset.variables["uReconstructY"][times,:,0]**2.) speed *= 3600. * 24. * 365. # convert from m/s to m/yr else: print('File does not contain surfaceSpeed or uReconstructX/Y.', @@ -93,7 +93,7 @@ if options.interp_temp: - temperature = dataset.variables['temperature'][:] + temperature = dataset.variables['temperature'][times,:] bedTopo = dataset.variables["bedTopography"][0,:] print('Reading bedTopography from the first time level only. If multiple', @@ -145,7 +145,7 @@ for i, time in enumerate(times): thk_interpolant = LinearNDInterpolator( - np.vstack((xCell, yCell)).T, thk[time,:]) + np.vstack((xCell, yCell)).T, thk[i,:]) thk_transect = thk_interpolant(np.vstack((xArray, yArray)).T) lower_surf = np.maximum( -910. / 1028. * thk_transect, bed_transect) lower_surf_nan = lower_surf.copy() # for plotting @@ -158,7 +158,7 @@ if plot_speed: speed_interpolant = LinearNDInterpolator( - np.vstack((xCell, yCell)).T, speed[time,:]) + np.vstack((xCell, yCell)).T, speed[i,:]) speed_transect = speed_interpolant(np.vstack((xArray, yArray)).T) speed_transect[thk_transect == 0.] = np.nan speedAx.plot(distance, speed_transect, color=timeColors[i]) @@ -180,7 +180,7 @@ print(f'Interpolating temperature for level {lev}') temp_interpolant = LinearNDInterpolator( np.vstack((xCell, yCell)).T, - temperature[time,:,lev]) + temperature[i,:,lev]) temp_transect[:, lev] = temp_interpolant( np.vstack((xArray, yArray)).T) From 55b906842c5ebbc19d2cb92136f6519f20ce5b19 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 14:21:42 -0800 Subject: [PATCH 063/169] Use nearest decimal year on colorbar --- landice/output_processing_li/plot_transect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index ad9dc3016..26ad4103d 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -55,7 +55,7 @@ if "daysSinceStart" in dataset.variables.keys(): use_yrs = True yrs = dataset.variables["daysSinceStart"][:] / 365. - times_list = [str(yrs[i]) for i in times] + times_list = [f'{yrs[i]:.1f}' for i in times] else: use_yrs = False From 6eb5e7fa39cac0fa314fa47d881137512fffb773 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 Nov 2023 19:31:21 -0800 Subject: [PATCH 064/169] Fix small bug that prints warning when using -c option --- landice/output_processing_li/plot_transect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 26ad4103d..73e1341c1 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -109,7 +109,7 @@ for row in reader: x.append(float(row[0])) y.append(float(row[1])) - if [options.x_coords, options.y_coords] is not [None, None]: + if [options.x_coords, options.y_coords] != [None, None]: print('-c and -x/-y options were both provided. Reading from ', f'{options.coords_file} and ignoring -x and -y settings.') else: From 24e089b01bbc6829e49bb29030d131e22ac06881 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 29 Nov 2023 14:11:35 -0800 Subject: [PATCH 065/169] Calculate distance using all transect points Calculate total distance using all transect points in plot_transect.py, rather than just estimating based on end points. --- landice/output_processing_li/plot_transect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 73e1341c1..3eaaf6fdc 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -112,12 +112,14 @@ if [options.x_coords, options.y_coords] != [None, None]: print('-c and -x/-y options were both provided. Reading from ', f'{options.coords_file} and ignoring -x and -y settings.') + x = np.asarray(x) + y = np.asarray(y) else: - x = [float(i) for i in options.x_coords.split(',')] - y = [float(i) for i in options.y_coords.split(',')] + x = np.array([float(i) for i in options.x_coords.split(',')]) + y = np.array([float(i) for i in options.y_coords.split(',')]) # increase sampling to match highest mesh resolution -total_distance = np.sqrt( (x[0] - x[1])**2. + (y[0] - y[1])**2.) +total_distance, = np.cumsum( np.sqrt( np.diff(x)**2. + np.diff(y)**2. ) ) n_samples = int(round(total_distance / np.min(dataset.variables["dcEdge"][:]))) xArray = np.interp(np.linspace(0, len(x)-1, n_samples), np.linspace(0, len(x)-1, len(x)), x) From a66f8091b7c5d79f684ffdd6a1a474404a87bbaf Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 29 Nov 2023 14:15:58 -0800 Subject: [PATCH 066/169] Change some variable names Change variable names from xArray and yArray to x_interp and y_interp to be more descriptive and more pythonesque. --- landice/output_processing_li/plot_transect.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/landice/output_processing_li/plot_transect.py b/landice/output_processing_li/plot_transect.py index 3eaaf6fdc..e4a5c7bac 100644 --- a/landice/output_processing_li/plot_transect.py +++ b/landice/output_processing_li/plot_transect.py @@ -121,15 +121,15 @@ # increase sampling to match highest mesh resolution total_distance, = np.cumsum( np.sqrt( np.diff(x)**2. + np.diff(y)**2. ) ) n_samples = int(round(total_distance / np.min(dataset.variables["dcEdge"][:]))) -xArray = np.interp(np.linspace(0, len(x)-1, n_samples), +x_interp = np.interp(np.linspace(0, len(x)-1, n_samples), np.linspace(0, len(x)-1, len(x)), x) -yArray = np.interp(np.linspace(0, len(y)-1, n_samples), +y_interp = np.interp(np.linspace(0, len(y)-1, n_samples), np.linspace(0, len(y)-1, len(y)), y) -d_distance = np.zeros(len(xArray)) -for ii in np.arange(1, len(xArray)): - d_distance[ii] = np.sqrt( (xArray[ii] - xArray[ii-1])**2 + - (yArray[ii] - yArray[ii-1])**2 ) +d_distance = np.zeros(len(x_interp)) +for ii in np.arange(1, len(x_interp)): + d_distance[ii] = np.sqrt( (x_interp[ii] - x_interp[ii-1])**2 + + (y_interp[ii] - y_interp[ii-1])**2 ) distance = np.cumsum(d_distance) / 1000. # in km for plotting @@ -143,12 +143,12 @@ plt.rcParams.update({'font.size': 16}) bed_interpolant = LinearNDInterpolator(np.vstack((xCell, yCell)).T, bedTopo) -bed_transect = bed_interpolant(np.vstack((xArray, yArray)).T) +bed_transect = bed_interpolant(np.vstack((x_interp, y_interp)).T) for i, time in enumerate(times): thk_interpolant = LinearNDInterpolator( np.vstack((xCell, yCell)).T, thk[i,:]) - thk_transect = thk_interpolant(np.vstack((xArray, yArray)).T) + thk_transect = thk_interpolant(np.vstack((x_interp, y_interp)).T) lower_surf = np.maximum( -910. / 1028. * thk_transect, bed_transect) lower_surf_nan = lower_surf.copy() # for plotting lower_surf_nan[thk_transect==0.] = np.nan @@ -161,7 +161,7 @@ if plot_speed: speed_interpolant = LinearNDInterpolator( np.vstack((xCell, yCell)).T, speed[i,:]) - speed_transect = speed_interpolant(np.vstack((xArray, yArray)).T) + speed_transect = speed_interpolant(np.vstack((x_interp, y_interp)).T) speed_transect[thk_transect == 0.] = np.nan speedAx.plot(distance, speed_transect, color=timeColors[i]) @@ -177,14 +177,14 @@ layer_thk[ii,0:-1]) / 2. layer_interfaces[ii,:] = upper_surf[ii] - layer_thk[ii,:] - temp_transect = np.zeros((len(xArray), nVertLevels)) + temp_transect = np.zeros((len(x_interp), nVertLevels)) for lev in range(nVertLevels): print(f'Interpolating temperature for level {lev}') temp_interpolant = LinearNDInterpolator( np.vstack((xCell, yCell)).T, temperature[i,:,lev]) temp_transect[:, lev] = temp_interpolant( - np.vstack((xArray, yArray)).T) + np.vstack((x_interp, y_interp)).T) thickAx.plot(distance, bed_transect, color='black') From eaad2348bc34994be24ab74950c10264917db741 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 1 Dec 2023 13:53:16 -0700 Subject: [PATCH 067/169] Add topo smoothing before init outside of ice-shelf cavities --- conda_package/recipe/meta.yaml | 3 + ocean/smooth_topo/CMakeLists.txt | 5 +- .../smooth_topo_before_init_skip_land_ice.F90 | 687 ++++++++++++++++++ 3 files changed, 694 insertions(+), 1 deletion(-) create mode 100644 ocean/smooth_topo/smooth_topo_before_init_skip_land_ice.F90 diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 584571918..3705ac1a0 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -96,6 +96,9 @@ test: - mpas_tools.viz - mpas_tools.conversion commands: + - test -f "${PREFIX}/bin/ocean_smooth_topo_skip_land_ice" + - test -f "${PREFIX}/bin/ocean_smooth_topo_before_init" + - test -f "${PREFIX}/bin/ocean_smooth_topo_before_init_skip_land_ice" - planar_hex --nx=10 --ny=20 --dc=1000. --outFileName='periodic_mesh_10x20_1km.nc' - translate_planar_grid -f 'periodic_mesh_10x20_1km.nc' -x 1000. -y 2000. - translate_planar_grid -f 'periodic_mesh_10x20_1km.nc' -c diff --git a/ocean/smooth_topo/CMakeLists.txt b/ocean/smooth_topo/CMakeLists.txt index 0026640de..1f69ffe0a 100644 --- a/ocean/smooth_topo/CMakeLists.txt +++ b/ocean/smooth_topo/CMakeLists.txt @@ -18,4 +18,7 @@ target_link_libraries (ocean_smooth_topo_skip_land_ice ${NETCDF_LIBRARIES}) add_executable (ocean_smooth_topo_before_init smooth_topo_before_init.F90) target_link_libraries (ocean_smooth_topo_before_init ${NETCDF_LIBRARIES}) -install (TARGETS ocean_smooth_topo_skip_land_ice ocean_smooth_topo_before_init DESTINATION bin) +add_executable (ocean_smooth_topo_before_init_skip_land_ice smooth_topo_before_init_skip_land_ice.F90) +target_link_libraries (ocean_smooth_topo_before_init_skip_land_ice ${NETCDF_LIBRARIES}) + +install (TARGETS ocean_smooth_topo_skip_land_ice ocean_smooth_topo_before_init ocean_smooth_topo_before_init_skip_land_ice DESTINATION bin) diff --git a/ocean/smooth_topo/smooth_topo_before_init_skip_land_ice.F90 b/ocean/smooth_topo/smooth_topo_before_init_skip_land_ice.F90 new file mode 100644 index 000000000..190c0663f --- /dev/null +++ b/ocean/smooth_topo/smooth_topo_before_init_skip_land_ice.F90 @@ -0,0 +1,687 @@ + +program smooth_topography + +! on compy +! module load gcc;module load netcdf +!gfortran smooth_topo_before_init.F90 -o a.out -I /share/apps/netcdf/4.6.3/gcc/4.8.5/include/ -L /share/apps/netcdf/4.6.3/gcc/4.8.5/lib/ -lnetcdf -lnetcdff + + use netcdf + + implicit none + + integer, parameter :: & + RKIND = selected_real_kind(13) + + ! input parameters + character(len=1000) :: & + filename_depth_in, & + filename_depth_out, & + filename_land_ice_mask, & + filename_mpas_mesh, & + filename_diagnostic + + real(kind=RKIND) :: & + layerBottom, dz, & + layerTop, partialCell_dz, & + maxBottomThicknessOrig, & + distanceLimit, & ! (km) + stdDeviation ! (km) + + integer :: & + numSmoothingPasses, smoothingPass + + namelist /smooth/ & + filename_depth_in, & + filename_depth_out, & + filename_land_ice_mask, & + filename_mpas_mesh, & + distanceLimit, & + stdDeviation, & + numSmoothingPasses + + ! mpas grid + integer :: & + nCells, & + nEdges, & + iCellFull, & + maxEdges + + real(kind=RKIND), dimension(:), allocatable :: & + xCell, & + yCell, & + zCell, & + lonCell, & + latCell, & + areaCell, & + bedElevationOrig, & + bedElevationNew, & + bedElevationPrev, & + dcEdge + + integer, dimension(:), allocatable :: & + landIceMask, & + nEdgesOnCell + + integer, dimension(:,:), allocatable :: & + cellsOnCell, & + edgesOnCell + + ! temporary variables + integer :: & + is, & + ia, & + iCell, & + k, & + start, & + count, & + ncid, & + varid_BedOrig, & + varid_BedNew + + real(kind=RKIND), dimension(:), allocatable :: & + weights, & + cellDistance, & + distanceSpread + + integer, dimension(:), allocatable :: & + iCellsSpread, & + cellStatus + + real(kind=RKIND), parameter :: & + km_to_m = 1000.0_RKIND + + ! diagnostic variables + real(kind=RKIND), dimension(:), allocatable :: & + weightsDiagnostic + + ! read in namelist + write(*,*) "Reading namelist..." + open(11,file="smooth_depth_in",status='old') + read(11,nml=smooth) + close(11) + + ! load the MPAS mesh file + call load_mesh_file() + + call load_topo_file() + + call load_land_ice_mask_file() + + ! allocate temporary arrays + write(*,*) "Allocate temporary variables..." + allocate(weights(nCells)) + allocate(cellDistance(nCells)) + allocate(distanceSpread(nCells)) + allocate(iCellsSpread(nCells)) + allocate(cellStatus(nCells)) + + write(*,*) "Perform smoothing..." + + bedElevationPrev = bedElevationOrig + bedElevationNew = bedElevationOrig ! for cells that do not get smoothed + +! convert to degrees + latCell = latCell*180./3.14159 + lonCell = lonCell*180./3.14159 + + do smoothingPass = 1, numSmoothingPasses + + write(*,*)' smoothing pass ',smoothingPass + + ! perform the smoothing + do iCell = 1, nCells + if (landIceMask(iCell) == 0) then + call smooth_topo(& + iCell, & + distanceLimit * km_to_m, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceSpread) + end if + enddo ! iCell + + bedElevationPrev = bedElevationNew + enddo ! smoothingPass + + ! output the depth file + call create_depth_file(filename_depth_out, ncid, & + varid_BedOrig, varid_BedNew) + + ! close the depth file + call close_depth_file(ncid) + +contains + + !---------------------------------------------------------------- + + function gaussian_weight(distance, stdDeviation) result(weight) + + real(kind=RKIND), intent(in) :: & + distance, & + stdDeviation + + real(kind=RKIND) :: weight + + weight = exp(-1.0_RKIND * (distance**2 / (2.0_RKIND * stdDeviation**2))) + + end function gaussian_weight + + !---------------------------------------------------------------- + + subroutine smooth_topo(& + iCell_Start, & + distanceLimit, & + nEdgesOnCell, & + cellsOnCell, & + edgesOnCell, & + dcEdge, & + cellStatus, & + cellDistance, & + distanceOut) + + integer, intent(in) :: & + iCell_Start + + real(kind=RKIND), intent(in) :: & + distanceLimit + + integer, dimension(:), intent(in) :: & + nEdgesOnCell + + integer, dimension(:,:), intent(in) :: & + cellsOnCell, & + edgesOnCell + + real(kind=RKIND), dimension(:), intent(in) :: & + dcEdge + + integer, dimension(:), intent(out) :: & + cellStatus + + real(kind=RKIND), dimension(:), intent(out) :: & + cellDistance, & + distanceOut + + integer :: & + nCellsOut + + integer, dimension(:), allocatable :: & + iCellsPrev, & + iCellsOut, & + iCellsNext + + integer :: & + nCellsPrev, & + nCellsNext, & + nLayer, & + iCellPrev, & + iCellOnCell, & + iCellNext, & + iEdge + + real(kind=RKIND) :: & + cellDistanceToAdd, & + weight, & + smoothedBedSum, & + weightSum + + allocate(iCellsPrev(nCells)) + allocate(iCellsNext(nCells)) + allocate(iCellsOut(nCells)) + + ! set the internal variables + cellStatus = -1 + cellDistance = -1.0_RKIND + + ! set first cell + cellStatus(iCell_Start) = 0 + nCellsPrev = 1 + iCellsPrev(1) = iCell_Start + + ! first output cell is the starting cell + nCellsOut = 1 + iCellsOut(1) = iCell_Start + distanceOut(1) = 0.0_RKIND + + ! initialize sums + smoothedBedSum = 0.0_RKIND + weightSum = 0.0_RKIND + + ! loop over cell layers from original cell + nLayer = 0 + do while (nCellsPrev > 0) + + ! reset number of cells found this layer + nCellsNext = 0 + + ! loop over cells defined in the previous iteration + do iCellPrev = 1, nCellsPrev + + ! loop over neighbours of these previous cells + do iCellOnCell = 1, nEdgesOnCell(iCellsPrev(iCellPrev)) + + ! get the iCell of the next potential cell in the next cells + iCellNext = cellsOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + ! get the edge index for this crossing + iEdge = edgesOnCell(iCellOnCell,iCellsPrev(iCellPrev)) + + cellDistanceToAdd = cellDistance(iCellsPrev(iCellPrev)) + dcEdge(iEdge) + + ! check to see if we need to add it to the next array + if (landIceMask(iCellNext) == 0 .and. cellStatus(iCellNext) == -1 .and. cellDistanceToAdd < distanceLimit) then + + ! count how many on the next list + nCellsNext = nCellsNext + 1 + + ! add this new cell to the next list + iCellsNext(nCellsNext) = iCellNext + + ! update the status of the cell + cellStatus(iCellNext) = nLayer + + ! calculate the distance to this cell + cellDistance(iCellNext) = cellDistanceToAdd + + ! output + nCellsOut = nCellsOut + 1 + iCellsOut(nCellsOut) = iCellNext + distanceOut(nCellsOut) = cellDistanceToAdd + + weight = gaussian_weight(cellDistanceToAdd, stdDeviation * km_to_m) + weightSum = weightSum + weight + smoothedBedSum = smoothedBedSum + weight*bedElevationPrev(iCellNext) + + endif ! cellStatus(iCellNext) == -1 + + enddo ! iCellOnCell + + enddo ! iCellPrev + + ! swap next and prev + nCellsPrev = nCellsNext + + iCellsPrev(1:nCellsNext) = iCellsnext(1:nCellsNext) + + ! increment the layer number + nLayer = nLayer + 1 + + enddo ! nCellsNext > 0 + + bedElevationNew(iCell_Start) = smoothedBedSum/weightSum + + deallocate(iCellsPrev) + deallocate(iCellsNext) + + end subroutine smooth_topo + + !---------------------------------------------------------------- + + subroutine create_depth_file(filename, ncid, & + varid_BedOrig, varid_BedNew) + + character(len=*), intent(in) :: & + filename + + integer, intent(out) :: & + ncid, & + varid_BedOrig, & + varid_BedNew + + integer :: & + status + + integer :: & + dimid_nCells, & + dimid_ni_a, & + dimid_nj_a, & + dimid_nv_a, & + dimid_src_grid_rank, & + dimid_n_b, & + dimid_ni_b, & + dimid_nj_b, & + dimid_nv_b, & + dimid_dst_grid_rank, & + dimid_n_s + + integer :: & + varid_xc_a, & + varid_yc_a, & + varid_xv_a, & + varid_yv_a, & + varid_mask_a, & + varid_area_a, & + varid_frac_a, & + varid_src_grid_dims, & + varid_xc_b, & + varid_yc_b, & + varid_xv_b, & + varid_yv_b, & + varid_mask_b, & + varid_area_b, & + varid_frac_b, & + varid_dst_grid_dims + + character(len=1000) :: & + date, & + time, & + zone, & + datetime + + integer, dimension(1) :: & + start1D, & + count1D + + integer, dimension(2) :: & + start2D, & + count2D + + write(*,*) "Create depth file...", trim(filename) + + ! create +! status = nf90_create(trim(filename), NF90_CLOBBER, ncid) + status = nf90_create(trim(filename), NF90_64BIT_OFFSET, ncid) + call netcdf_error(status, "create_depth_file: nf90_open") + + ! define dimensions + ! nCells + status = nf90_def_dim(ncid, "nCells", nCells, dimid_nCells) + call netcdf_error(status, "create_depth_file: nf90_def_dim nCells") + + ! define variables + ! bedElevationOrig + status = nf90_def_var(ncid, "bed_elevationOrig", NF90_DOUBLE, (/dimid_nCells/), varid_BedOrig) + call netcdf_error(status, "create_depth_file: nf90_def_var bedElevationOrig") + + status = nf90_put_att(ncid, varid_BedOrig, "long_name", & + "Elevation of the bottom of the ocean. Given as a negative distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationOrig") + + status = nf90_put_att(ncid, varid_BedOrig, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationOrig") + + ! bedElevationNew + status = nf90_def_var(ncid, "bed_elevationNew", NF90_DOUBLE, (/dimid_nCells/), varid_BedNew) + call netcdf_error(status, "create_depth_file: nf90_def_var bedElevationNew") + + status = nf90_put_att(ncid, varid_BedNew, "long_name", & + "Elevation of the bottom of the ocean. Given as a negative distance from sea level.") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationNew") + + status = nf90_put_att(ncid, varid_BedNew, "units", "m") + call netcdf_error(status, "create_depth_file: nf90_put_att bedElevationNew") + + ! global attributes + status = nf90_put_att(ncid, NF90_GLOBAL, "input_depth_file", trim(filename_depth_in)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_depth_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "input_mesh_file", trim(filename_mpas_mesh)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_mesh_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "input_land_ice_mask_file", trim(filename_land_ice_mask)) + call netcdf_error(status, "create_depth_file: nf90_put_att input_land_ice_mask_file") + + status = nf90_put_att(ncid, NF90_GLOBAL, "smoothing_method", "2D Gaussian smoothing") + call netcdf_error(status, "create_depth_file: nf90_put_att smoothing_method") + + status = nf90_put_att(ncid, NF90_GLOBAL, "created_by", "smooth_topo_before_init") + call netcdf_error(status, "create_depth_file: nf90_put_att created_by") + + call date_and_time(date, time, zone) + datetime = date(1:4)//"-"//date(5:6)//"-"//date(7:8)//"_"//time(1:2)//":"//time(3:4)//":"//time(5:6)//" "//trim(zone) + status = nf90_put_att(ncid, NF90_GLOBAL, "created_at", trim(datetime)) + call netcdf_error(status, "create_depth_file: nf90_put_att created_at") + + status = nf90_put_att(ncid, NF90_GLOBAL, "distanceLimit", distanceLimit) + call netcdf_error(status, "create_depth_file: nf90_put_att distanceLimit") + + status = nf90_put_att(ncid, NF90_GLOBAL, "stdDeviation", stdDeviation) + call netcdf_error(status, "create_depth_file: nf90_put_att stdDeviation") + + ! end definition phase + status = nf90_enddef(ncid) + call netcdf_error(status, "create_depth_file: nf90_enddef") + + ! write variables + start1D(1) = 1 + count1D(1) = nCells + + ! bedElevationOrig + status = nf90_put_var(ncid, varid_BedOrig, bedElevationOrig, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var bedElevationOrig") + + ! bedElevationNew + status = nf90_put_var(ncid, varid_BedNew, bedElevationNew, start1D, count1D) + call netcdf_error(status, "create_depth_file: nf90_put_var bedElevationNew") + + end subroutine create_depth_file + + !---------------------------------------------------------------- + + subroutine close_depth_file(ncid) + + integer, intent(in) :: & + ncid + + integer :: & + status + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "close_depth_file: nf90_close ") + + end subroutine close_depth_file + + !---------------------------------------------------------------- + + subroutine load_mesh_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load mesh file..." + + ! open file + status = nf90_open(trim(filename_mpas_mesh), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_mesh_file: nf90_open") + + ! nCells + status = nf90_inq_dimid(ncid, "nCells", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nCells") + + status = nf90_inquire_dimension(ncid, dimid, len=nCells) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nCells") + + ! nEdges + status = nf90_inq_dimid(ncid, "nEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid nEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=nEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension nEdges") + + ! maxEdges + status = nf90_inq_dimid(ncid, "maxEdges", dimid) + call netcdf_error(status, "load_mesh_file: nf90_inq_dimid maxEdges") + + status = nf90_inquire_dimension(ncid, dimid, len=maxEdges) + call netcdf_error(status, "load_mesh_file: nf90_inquire_dimension maxEdges") + + allocate(xCell(nCells)) + allocate(yCell(nCells)) + allocate(zCell(nCells)) + allocate(latCell(nCells)) + allocate(lonCell(nCells)) + allocate(areaCell(nCells)) + allocate(dcEdge(nEdges)) + allocate(nEdgesOnCell(nCells)) + allocate(cellsOnCell(maxEdges,nCells)) + allocate(edgesOnCell(maxEdges,nCells)) + + ! xCell + status = nf90_inq_varid(ncid, "xCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid xCell") + + status = nf90_get_var(ncid, varid, xCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var xCell") + + ! yCell + status = nf90_inq_varid(ncid, "yCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid yCell") + + status = nf90_get_var(ncid, varid, yCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var yCell") + + ! zCell + status = nf90_inq_varid(ncid, "zCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid zCell") + + status = nf90_get_var(ncid, varid, zCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var zCell") + + ! latCell + status = nf90_inq_varid(ncid, "latCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid latCell") + + status = nf90_get_var(ncid, varid, latCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var latCell") + + ! lonCell + status = nf90_inq_varid(ncid, "lonCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid lonCell") + + status = nf90_get_var(ncid, varid, lonCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var lonCell") + + ! areaCell + status = nf90_inq_varid(ncid, "areaCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid areaCell") + + status = nf90_get_var(ncid, varid, areaCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var areaCell") + + ! dcEdge + status = nf90_inq_varid(ncid, "dcEdge", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid dcEdge") + + status = nf90_get_var(ncid, varid, dcEdge) + call netcdf_error(status, "load_mesh_file: nf90_get_var dcEdge") + + ! nEdgesOnCell + status = nf90_inq_varid(ncid, "nEdgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid nEdgesOnCell") + + status = nf90_get_var(ncid, varid, nEdgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var nEdgesOnCell") + + ! cellsOnCell + status = nf90_inq_varid(ncid, "cellsOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid cellsOnCell") + + status = nf90_get_var(ncid, varid, cellsOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var cellsOnCell") + + ! edgesOnCell + status = nf90_inq_varid(ncid, "edgesOnCell", varid) + call netcdf_error(status, "load_mesh_file: nf90_inq_varid edgesOnCell") + + status = nf90_get_var(ncid, varid, edgesOnCell) + call netcdf_error(status, "load_mesh_file: nf90_get_var edgesOnCell") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_mesh_file: nf90_close") + + end subroutine load_mesh_file + + !---------------------------------------------------------------- + + subroutine load_topo_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load topo file..." + + ! open file + status = nf90_open(trim(filename_depth_in), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_topo_file: nf90_open") + + allocate(bedElevationOrig(nCells)) + allocate(bedElevationNew(nCells)) + allocate(bedElevationPrev(nCells)) + + ! bedElevation + status = nf90_inq_varid(ncid, "bed_elevation", varid) + call netcdf_error(status, "load_topo_file: nf90_inq_varid bed_elevation") + + status = nf90_get_var(ncid, varid, bedElevationOrig) + call netcdf_error(status, "load_topo_file: nf90_get_var bed_elevation") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_topo_file: nf90_close") + + end subroutine load_topo_file + + !---------------------------------------------------------------- + + subroutine load_land_ice_mask_file() + + integer :: & + status, & + ncid, & + dimid, & + varid + + write(*,*) "Load land-ice mask file..." + + ! open file + status = nf90_open(trim(filename_land_ice_mask), NF90_NOWRITE, ncid) + call netcdf_error(status, "load_land_ice_mask_file: nf90_open") + + allocate(landIceMask(nCells)) + + ! bedElevation + status = nf90_inq_varid(ncid, "landIceMask", varid) + call netcdf_error(status, "load_land_ice_mask_file: nf90_inq_varid landIceMask") + + status = nf90_get_var(ncid, varid, landIceMask) + call netcdf_error(status, "load_land_ice_mask_file: nf90_get_var landIceMask") + + ! close + status = nf90_close(ncid) + call netcdf_error(status, "load_land_ice_mask_file: nf90_close") + + end subroutine load_land_ice_mask_file + + !---------------------------------------------------------------- + + subroutine netcdf_error(status, message) + + integer, intent(in) :: & + status + + character(len=*), intent(in) :: & + message + + if (status /= 0) then + write(*,*) "Netcdf error: ", status, nf90_strerror(status) + write(*,*) trim(message) + stop + endif + + end subroutine netcdf_error + + !---------------------------------------------------------------- + +end program smooth_topography From 4cd8d35c5dd3dc134ead7fc57329d9925a653921 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 1 Dec 2023 18:02:59 -0700 Subject: [PATCH 068/169] Update to v0.29.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index e7c60d636..24b674684 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -41,6 +41,7 @@ Documentation On GitHub `v0.26.0`_ `0.26.0`_ `v0.27.0`_ `0.27.0`_ `v0.28.0`_ `0.28.0`_ +`v0.29.0`_ `0.29.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -113,3 +114,5 @@ Documentation On GitHub .. _`0.27.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.27.0 .. _`v0.28.0`: ../0.28.0/index.html .. _`0.28.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.28.0 +.. _`v0.29.0`: ../0.29.0/index.html +.. _`0.29.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.29.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 15d81c7a1..fe5c126e0 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 28, 0) +__version_info__ = (0, 29, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 3705ac1a0..90da4f40f 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.28.0" %} +{% set version = "0.29.0" %} package: name: {{ name|lower }} From 5359e8f07bc425f37cd4a209b9c7797d2bca9053 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Wed, 13 Dec 2023 08:18:40 -0800 Subject: [PATCH 069/169] Add check that basalHeatFlux is non-negative Add check that basalHeatFlux is non-negative. If any negative values are found, throw an error and write out a message to check the source data set. --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index aced90c70..4d06cbe63 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -756,6 +756,12 @@ def vertical_interp_MPAS_grid(mpas_grid_input_layers, destVertCoord, input_layer MPASfield[MPASfield < 0.0] = 0.0 print(' removed negative thickness, new min/max: {} {}'.format(MPASfield.min(), MPASfield.max())) + # basalHeatFlux must be non-negative + if MPASfieldName == 'basalHeatFlux': + assert MPASfield.min() >= 0.0, 'basalHeatFlux contains negative values! This is likely due to the ' \ + 'conventions used in the input file, rather than bad data. Ensure ' \ + 'non-negative values before interpolating.' + # Now insert the MPAS field into the file. if 'Time' in MPASfile.variables[MPASfieldName].dimensions: MPASfile.variables[MPASfieldName][timelevout,:] = MPASfield # Time will always be leftmost index From 880e35f36872fdd02ddb8c4b02b3d1e0f5231330 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 16 Dec 2023 20:34:35 -0700 Subject: [PATCH 070/169] Fix bug in indexing when using esmf interplation This bug yielded garbage results when interpolating between two MPAS meshes. When interpolating from a regular grid to an MPAS grid, it would cause a shift of one cell on the regular grid. --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index aced90c70..c9e07cad5 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -84,7 +84,7 @@ def ESMF_interp(sourceField): destinationField = np.zeros(xCell.shape) # fields on cells only sourceFieldFlat = sourceField.flatten() # Flatten source field for i in range(len(row)): - destinationField[row[i]-1] = destinationField[row[i]-1] + S[i] * sourceFieldFlat[col[i]] + destinationField[row[i]-1] = destinationField[row[i]-1] + S[i] * sourceFieldFlat[col[i]-1] except: 'error in ESMF_interp' return destinationField From 4cbe511c1b2e34d54bbd59c32506ad91e76558a6 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 16 Dec 2023 22:30:56 -0700 Subject: [PATCH 071/169] Refactor loop to use scipy sparse matrix multiply For interpolating from 4km AIS (385379 cells) to 8km AIS (98341 cells) this reduced interpolation time for one variable from 13 seconds to 0.04 seconds (~300x speedup). --- .../interpolate_to_mpasli_grid.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index c9e07cad5..872b3b2e7 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -32,6 +32,7 @@ import math from collections import OrderedDict import scipy.spatial +import scipy.sparse import time from datetime import datetime @@ -64,9 +65,14 @@ S = wfile.variables['S'][:] col = wfile.variables['col'][:] row = wfile.variables['row'][:] + n_a = len(wfile.dimensions['n_a']) + n_b = len(wfile.dimensions['n_b']) wfile.close() #---------------------------- + # convert to sparse matrix format + wt_csr = scipy.sparse.coo_array((S, (row - 1, col - 1)), shape=(n_b, n_a)).tocsr() + print('') # make a space in stdout before further output @@ -78,15 +84,13 @@ #---------------------------- def ESMF_interp(sourceField): - # Interpolates from the sourceField to the destinationField using ESMF weights + # Interpolates from the sourceField to the destinationField using ESMF weights + destinationField = np.zeros(xCell.shape) # fields on cells only try: - # Initialize new field to 0 - required - destinationField = np.zeros(xCell.shape) # fields on cells only - sourceFieldFlat = sourceField.flatten() # Flatten source field - for i in range(len(row)): - destinationField[row[i]-1] = destinationField[row[i]-1] + S[i] * sourceFieldFlat[col[i]-1] + source_csr = scipy.sparse.csr_matrix(sourceField.flatten()[:, np.newaxis]) + destinationField = wt_csr.dot(source_csr).toarray() except: - 'error in ESMF_interp' + print('error in ESMF_interp') return destinationField #---------------------------- From 625e5058ba0cc42cab40c83a74da4e4a3a9a3e33 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 16 Dec 2023 23:07:03 -0700 Subject: [PATCH 072/169] Add support for ESMF interp of MPAS fields with a vertical dimension This was never attempted before and needed additional handling. --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index 872b3b2e7..87edbe6d4 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -88,7 +88,7 @@ def ESMF_interp(sourceField): destinationField = np.zeros(xCell.shape) # fields on cells only try: source_csr = scipy.sparse.csr_matrix(sourceField.flatten()[:, np.newaxis]) - destinationField = wt_csr.dot(source_csr).toarray() + destinationField = wt_csr.dot(source_csr).toarray().squeeze() except: print('error in ESMF_interp') return destinationField @@ -332,7 +332,7 @@ def interpolate_field_with_layers(MPASfieldName): if filetype=='cism': print(' Input layer {}, layer {} min/max: {} {}'.format(z, InputFieldName, InputField[z,:,:].min(), InputField[z,:,:].max())) elif filetype=='mpas': - print(' Input layer {}, layer {} min/max: {} {}'.format(z, InputFieldName, InputField[:,z].min(), InputField[z,:].max())) + print(' Input layer {}, layer {} min/max: {} {}'.format(z, InputFieldName, InputField[:,z].min(), InputField[:,z].max())) # Call the appropriate routine for actually doing the interpolation if args.interpType == 'b': print(" ...Layer {}, Interpolating this layer to MPAS grid using built-in bilinear method...".format(z)) @@ -353,7 +353,10 @@ def interpolate_field_with_layers(MPASfieldName): mpas_grid_input_layers[z,:] = InputField[:,z].flatten()[nn_idx_cell] # 2d cism fields need to be flattened. (Note the indices were flattened during init, so this just matches that operation for the field data itself.) 1d mpas fields do not, but the operation won't do anything because they are already flat. elif args.interpType == 'e': print(" ...Layer{}, Interpolating this layer to MPAS grid using ESMF-weights method...".format(z)) - mpas_grid_input_layers[z,:] = ESMF_interp(InputField[z,:,:]) + if filetype=='cism': + mpas_grid_input_layers[z,:] = ESMF_interp(InputField[z,:,:]) + elif filetype=='mpas': + mpas_grid_input_layers[z,:] = ESMF_interp(InputField[:,z]) else: sys.exit('ERROR: Unknown interpolation method specified') print(' interpolated MPAS {}, layer {} min/max {} {}: '.format(MPASfieldName, z, mpas_grid_input_layers[z,:].min(), mpas_grid_input_layers[z,:].max())) From 3cd10a273620a979c3ffebc9ec7faff12d84505c Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Sat, 16 Dec 2023 23:20:49 -0700 Subject: [PATCH 073/169] Add dst_area normalization See section 12.5 of ESMF_RegridWeightGen documentation: https://earthsystemmodeling.org/docs/release/ESMF_8_0_1/ESMF_refdoc/node3.html#SECTION03025000000000000000 --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index 87edbe6d4..a0ddb7c2c 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -67,6 +67,7 @@ row = wfile.variables['row'][:] n_a = len(wfile.dimensions['n_a']) n_b = len(wfile.dimensions['n_b']) + dst_frac = wfile.variables['frac_b'][:] wfile.close() #---------------------------- @@ -89,6 +90,10 @@ def ESMF_interp(sourceField): try: source_csr = scipy.sparse.csr_matrix(sourceField.flatten()[:, np.newaxis]) destinationField = wt_csr.dot(source_csr).toarray().squeeze() + # For conserve remapping, need to normalize by destination area fraction + # It should be safe to do this for other methods + ind = np.where(dst_frac > 0.0)[0] + destinationField[ind] /= dst_frac[ind] except: print('error in ESMF_interp') return destinationField From 3cc39403e7f76b91e987342f1880e9d408d1bf28 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Mon, 18 Dec 2023 15:51:38 -0700 Subject: [PATCH 074/169] Add support for variables with vertical dimension The entire masking and extrapolation logic has been moved into a loop over vertical levels. It seemed conceivable that the masking criteria could differ between different levels of the same variable, which is why the masking operation is also moved inside the vertical level loop. --- landice/mesh_tools_li/extrapolate_variable.py | 171 ++++++++++-------- 1 file changed, 97 insertions(+), 74 deletions(-) diff --git a/landice/mesh_tools_li/extrapolate_variable.py b/landice/mesh_tools_li/extrapolate_variable.py index e5c883f35..3c0f63d62 100644 --- a/landice/mesh_tools_li/extrapolate_variable.py +++ b/landice/mesh_tools_li/extrapolate_variable.py @@ -31,7 +31,6 @@ dataset = Dataset(options.nc_file, 'r+') dataset.set_auto_mask(False) var_name = options.var_name -varValue = dataset.variables[var_name][0, :] extrapolation = options.extrapolation # Extrapolation nCells = len(dataset.dimensions['nCells']) @@ -43,80 +42,104 @@ xCell = dataset.variables["yCell"][:] yCell = dataset.variables["xCell"][:] -# Define region of good data to extrapolate from. Different methods for different variables -if var_name in ["effectivePressure", "beta", "muFriction"]: - groundedMask = (thickness > (-1028.0 / 910.0 * bed)) - keepCellMask = np.copy(groundedMask) * np.isfinite(varValue) - extrapolation == "min" - - for iCell in range(nCells): - for n in range(nEdgesOnCell[iCell]): - jCell = cellsOnCell[iCell, n] - 1 - if (groundedMask[jCell] == 1): - keepCellMask[iCell] = 1 - continue - keepCellMask *= (varValue > 0) # ensure zero muFriction does not get extrapolated - keepCellMask *= (varValue < 1.e12) # get rid of ridiculous values -elif var_name in ["floatingBasalMassBal"]: - floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) - keepCellMask = floatingMask * (varValue != 0.0) - extrapolation == "idw" +if dataset.variables[var_name].ndim == 2: + has_vertical_dim = False + n_vert = 1 + varValue = dataset.variables[var_name][0, :] +elif dataset.variables[var_name].ndim == 3: + has_vertical_dim = True + print(dataset.variables[var_name].dimensions[2]) + vert_dim_name = dataset.variables[var_name].dimensions[2] + n_vert = len(dataset.dimensions[vert_dim_name]) + print(f"This variable has a vertical dimension of {vert_dim_name} with size {n_vert}") + print("") else: - keepCellMask = (thickness > 0.0) - -keepCellMaskNew = np.copy(keepCellMask) # make a copy to edit that will be used later -keepCellMaskOrig = np.copy(keepCellMask) # make a copy to edit that can be edited without changing the original - -# recursive extrapolation steps: -# 1) find cell A with mask = 0 -# 2) find how many surrounding cells have nonzero mask, and their indices (this will give us the cells from upstream) -# 3) use the values for nonzero mask upstream cells to extrapolate the value for cell A -# 4) change the mask for A from 0 to 1 -# 5) Update mask -# 6) go to step 1) - -print("\nStart {} extrapolation using {} method".format(var_name, extrapolation)) -if extrapolation == 'value': - varValue[np.where(np.logical_not(keepCellMask))] = float(options.set_value) -else: - while np.count_nonzero(keepCellMask) != nCells: - keepCellMask = np.copy(keepCellMaskNew) - searchCells = np.where(keepCellMask==0)[0] - varValueOld = np.copy(varValue) - - for iCell in searchCells: - neighborcellID = cellsOnCell[iCell,:nEdgesOnCell[iCell]]-1 - neighborcellID = neighborcellID[neighborcellID>=0] # Important: ignore the phantom "neighbors" that are off the edge of the mesh (0 values in cellsOnCell) - - mask_for_idx = keepCellMask[neighborcellID] # active cell mask - mask_nonzero_idx, = np.nonzero(mask_for_idx) - - nonzero_id = neighborcellID[mask_nonzero_idx] # id for nonzero beta cells - nonzero_num = np.count_nonzero(mask_for_idx) - - assert len(nonzero_id) == nonzero_num - - if nonzero_num > 0: - x_i = xCell[iCell] - y_i = yCell[iCell] - x_adj = xCell[nonzero_id] - y_adj = yCell[nonzero_id] - var_adj = varValueOld[nonzero_id] - if extrapolation == 'idw': - ds = np.sqrt((x_i-x_adj)**2+(y_i-y_adj)**2) - assert np.count_nonzero(ds)==len(ds) - var_interp = 1.0/sum(1.0/ds)*sum(1.0/ds*var_adj) - varValue[iCell] = var_interp - elif extrapolation == 'min': - varValue[iCell] = np.min(var_adj) - else: - sys.exit("ERROR: wrong extrapolation scheme! Set option m as idw or min!") - - keepCellMaskNew[iCell] = 1 - - print ("{0:8d} cells left for extrapolation in total {1:8d} cells".format(nCells-np.count_nonzero(keepCellMask), nCells)) - -dataset.variables[var_name][0,:] = varValue # Put updated array back into file. + print("Unexpected number of dimension in variable") + +for v in range(n_vert): + if has_vertical_dim == True: + print(f"Processing vertical level number {v}") + varValue = dataset.variables[var_name][0, :, v] + else: + varValue = dataset.variables[var_name][0, :] + + # Define region of good data to extrapolate from. Different methods for different variables + if var_name in ["effectivePressure", "beta", "muFriction"]: + groundedMask = (thickness > (-1028.0 / 910.0 * bed)) + keepCellMask = np.copy(groundedMask) * np.isfinite(varValue) + extrapolation == "min" + + for iCell in range(nCells): + for n in range(nEdgesOnCell[iCell]): + jCell = cellsOnCell[iCell, n] - 1 + if (groundedMask[jCell] == 1): + keepCellMask[iCell] = 1 + continue + keepCellMask *= (varValue > 0) # ensure zero muFriction does not get extrapolated + keepCellMask *= (varValue < 1.e12) # get rid of ridiculous values + elif var_name in ["floatingBasalMassBal"]: + floatingMask = (thickness <= (-1028.0 / 910.0 * bed)) + keepCellMask = floatingMask * (varValue != 0.0) + extrapolation == "idw" + else: + keepCellMask = (thickness > 0.0) + + keepCellMaskNew = np.copy(keepCellMask) # make a copy to edit that will be used later + keepCellMaskOrig = np.copy(keepCellMask) # make a copy to edit that can be edited without changing the original + + # recursive extrapolation steps: + # 1) find cell A with mask = 0 + # 2) find how many surrounding cells have nonzero mask, and their indices (this will give us the cells from upstream) + # 3) use the values for nonzero mask upstream cells to extrapolate the value for cell A + # 4) change the mask for A from 0 to 1 + # 5) Update mask + # 6) go to step 1) + + print("\nStart {} extrapolation using {} method".format(var_name, extrapolation)) + if extrapolation == 'value': + varValue[np.where(np.logical_not(keepCellMask))] = float(options.set_value) + else: + while np.count_nonzero(keepCellMask) != nCells: + keepCellMask = np.copy(keepCellMaskNew) + searchCells = np.where(keepCellMask==0)[0] + varValueOld = np.copy(varValue) + + for iCell in searchCells: + neighborcellID = cellsOnCell[iCell,:nEdgesOnCell[iCell]]-1 + neighborcellID = neighborcellID[neighborcellID>=0] # Important: ignore the phantom "neighbors" that are off the edge of the mesh (0 values in cellsOnCell) + + mask_for_idx = keepCellMask[neighborcellID] # active cell mask + mask_nonzero_idx, = np.nonzero(mask_for_idx) + + nonzero_id = neighborcellID[mask_nonzero_idx] # id for nonzero beta cells + nonzero_num = np.count_nonzero(mask_for_idx) + + assert len(nonzero_id) == nonzero_num + + if nonzero_num > 0: + x_i = xCell[iCell] + y_i = yCell[iCell] + x_adj = xCell[nonzero_id] + y_adj = yCell[nonzero_id] + var_adj = varValueOld[nonzero_id] + if extrapolation == 'idw': + ds = np.sqrt((x_i-x_adj)**2+(y_i-y_adj)**2) + assert np.count_nonzero(ds)==len(ds) + var_interp = 1.0/sum(1.0/ds)*sum(1.0/ds*var_adj) + varValue[iCell] = var_interp + elif extrapolation == 'min': + varValue[iCell] = np.min(var_adj) + else: + sys.exit("ERROR: wrong extrapolation scheme! Set option m as idw or min!") + + keepCellMaskNew[iCell] = 1 + + print ("{0:8d} cells left for extrapolation in total {1:8d} cells".format(nCells-np.count_nonzero(keepCellMask), nCells)) + + if has_vertical_dim == True: + dataset.variables[var_name][0,:,v] = varValue # Put updated array back into file. + else: + dataset.variables[var_name][0,:] = varValue # Put updated array back into file. # === Clean-up ============= # Update history attribute of netCDF file From abdc035add6359442e43090cc773045dc1490841 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 20 Dec 2023 23:45:50 -0700 Subject: [PATCH 075/169] Refactor create_SCRIP_file_from_planar_rectangular_grid.py For large grids, this script was very slow. For the 500m BedMachine-AIS dataset, it's been running for over half an hour and is not complete. After refactoring, it took about 3 minutes. --- ...SCRIP_file_from_planar_rectangular_grid.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/mesh_tools/create_SCRIP_files/create_SCRIP_file_from_planar_rectangular_grid.py b/mesh_tools/create_SCRIP_files/create_SCRIP_file_from_planar_rectangular_grid.py index 4a8ce9584..8d42597c5 100755 --- a/mesh_tools/create_SCRIP_files/create_SCRIP_file_from_planar_rectangular_grid.py +++ b/mesh_tools/create_SCRIP_files/create_SCRIP_file_from_planar_rectangular_grid.py @@ -125,18 +125,19 @@ print ('Filling in corners of each cell.') grid_corner_lon_local = np.zeros( (nx * ny, 4) ) # It is WAYYY faster to fill in the array entry-by-entry in memory than to disk. grid_corner_lat_local = np.zeros( (nx * ny, 4) ) -for j in range(ny): - for i in range(nx): - iCell = j*nx + i - - grid_corner_lon_local[iCell, 0] = stag_lon[j, i] - grid_corner_lon_local[iCell, 1] = stag_lon[j, i+1] - grid_corner_lon_local[iCell, 2] = stag_lon[j+1, i+1] - grid_corner_lon_local[iCell, 3] = stag_lon[j+1, i] - grid_corner_lat_local[iCell, 0] = stag_lat[j, i] - grid_corner_lat_local[iCell, 1] = stag_lat[j, i+1] - grid_corner_lat_local[iCell, 2] = stag_lat[j+1, i+1] - grid_corner_lat_local[iCell, 3] = stag_lat[j+1, i] + +jj = np.arange(ny) +ii = np.arange(nx) +i_ind, j_ind = np.meshgrid(ii, jj) +cell_ind = j_ind * nx + i_ind +grid_corner_lon_local[cell_ind, 0] = stag_lon[j_ind, i_ind] +grid_corner_lon_local[cell_ind, 1] = stag_lon[j_ind, i_ind + 1] +grid_corner_lon_local[cell_ind, 2] = stag_lon[j_ind + 1, i_ind + 1] +grid_corner_lon_local[cell_ind, 3] = stag_lon[j_ind + 1, i_ind] +grid_corner_lat_local[cell_ind, 0] = stag_lat[j_ind, i_ind] +grid_corner_lat_local[cell_ind, 1] = stag_lat[j_ind, i_ind + 1] +grid_corner_lat_local[cell_ind, 2] = stag_lat[j_ind + 1, i_ind + 1] +grid_corner_lat_local[cell_ind, 3] = stag_lat[j_ind + 1, i_ind] grid_corner_lon[:] = grid_corner_lon_local[:] grid_corner_lat[:] = grid_corner_lat_local[:] @@ -171,3 +172,4 @@ fin.close() fout.close() +print('scrip file generation complete') From f758259aa2d2ba5765eb6f786a01ffa840b24a77 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Thu, 21 Dec 2023 00:34:17 -0700 Subject: [PATCH 076/169] Fix print statements in landice cull mask script --- landice/mesh_tools_li/define_cullMask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landice/mesh_tools_li/define_cullMask.py b/landice/mesh_tools_li/define_cullMask.py index 5c829f929..bea6c5c86 100755 --- a/landice/mesh_tools_li/define_cullMask.py +++ b/landice/mesh_tools_li/define_cullMask.py @@ -101,7 +101,7 @@ if keepCellMask[n] == 1: keepCellMaskNew[iCell] = 1 keepCellMask = np.copy(keepCellMaskNew) # after we've looped over all cells assign the new mask to the variable we need (either for another loop around the domain or to write out) - print(' Num of cells to keep: {}'.format(sum(keepCellMask))) + print(f'Num of cells to keep: {keepCellMask.sum()}') # Now convert the keepCellMask to the cullMask cullCell[:] = np.absolute(keepCellMask[:]-1) # Flip the mask for which ones to cull @@ -148,7 +148,7 @@ ind = np.nonzero(((xCell-xCell[iCell])**2 + (yCell-yCell[iCell])**2)**0.5 < dist)[0] keepCellMask[ind] = 1 - print(' Num of cells to keep:'.format(sum(keepCellMask))) + print(f'Num of cells to keep: {keepCellMask.sum()}') # Now convert the keepCellMask to the cullMask cullCell[:] = np.absolute(keepCellMask[:]-1) # Flip the mask for which ones to cull From a19962aab2371da13c809b456ffd77d2ab559135 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 3 Jan 2024 12:25:19 -0800 Subject: [PATCH 077/169] Fix bug in bulldoze_missing_troughs.py Not sure how this was committed like this - it won't run as is. --- landice/mesh_tools_li/bulldoze_missing_troughs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landice/mesh_tools_li/bulldoze_missing_troughs.py b/landice/mesh_tools_li/bulldoze_missing_troughs.py index 671eaeca3..8620e8aad 100644 --- a/landice/mesh_tools_li/bulldoze_missing_troughs.py +++ b/landice/mesh_tools_li/bulldoze_missing_troughs.py @@ -70,9 +70,9 @@ def smoothTrough(WCT, p1, p2, minWCT, maxWCT): WCTnew = WCT.copy() # find representative dCell for this area - ind = np.nonzero( (xEdge>p1[0]) * (xEdgep1[1]) * (yEdgep1[0]) * (xEdgep1[1]) * (yEdge(p1[0]-2*dCell)) * (xCell<(p2[0]+2*dCell)) * (yCell>(p1[1]-2*dCell)) * (yCell<(p2[1]+2*dCell)) ind = np.nonzero(mask==1)[0] From 143c3d60a448462c5888c63903f85483e6160a0f Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 3 Jan 2024 12:26:12 -0800 Subject: [PATCH 078/169] Reduce verbosity of bulldoze_missing_troughs.py For high res meshes, the existing stdout destroys the terminal history --- landice/mesh_tools_li/bulldoze_missing_troughs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landice/mesh_tools_li/bulldoze_missing_troughs.py b/landice/mesh_tools_li/bulldoze_missing_troughs.py index 8620e8aad..78f0a88d2 100644 --- a/landice/mesh_tools_li/bulldoze_missing_troughs.py +++ b/landice/mesh_tools_li/bulldoze_missing_troughs.py @@ -89,7 +89,7 @@ def smoothTrough(WCT, p1, p2, minWCT, maxWCT): dist2centerline = np.absolute(m1*xCell[ind2] + -1*yCell[ind2] + b1) / (m1**2 + (-1)**2)**0.5 #dist2centerline = np.absolute( (p2[0]-p1[0]) * (p1[1] - yCell[ind2]) - (p1[0]-xCell[ind2])*(p2[1]-p1[1])) / ( (p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)**0.5 dist2centerline *= np.sign((m1*xCell[ind2]+b1) - yCell[ind2]) # make signed distance, assuming NE direction - print(dist2centerline) + #print(dist2centerline) width = dist2centerline.max()-dist2centerline.min() # find frac x along center line @@ -102,7 +102,7 @@ def smoothTrough(WCT, p1, p2, minWCT, maxWCT): widthEndIdx = ind2[np.argmax(dist2centerline)] #idx of westmost widthEnd = (xCell[widthEndIdx], yCell[widthEndIdx]) # position of westmost widthMidpt = ((widthOrgn[0]+widthEnd[0])/2.0, (widthOrgn[1]+widthEnd[1])/2.0) - print(xCell[c], yCell[c], widthOrgn) + #print(xCell[c], yCell[c], widthOrgn) #dist2Orgn = ((widthOrgn[0]-xCell[c])**2 + (widthOrgn[1]-yCell[c])**2)**0.5 #fracWidth = (dist2Orgn / (width) - 0.5) * 2.0 # [-1,1] range dist2Mid = ((widthMidpt[0]-xCell[c])**2 + (widthMidpt[1]-yCell[c])**2)**0.5 From f25861e2d7d33ea449206984660bea8482bd4d55 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 3 Jan 2024 12:27:11 -0800 Subject: [PATCH 079/169] Change defaults in scripts to match what was done to the AIS 4km mesh --- landice/mesh_tools_li/adjust_iceshelf_geometry.py | 2 +- landice/mesh_tools_li/bulldoze_missing_troughs.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/landice/mesh_tools_li/adjust_iceshelf_geometry.py b/landice/mesh_tools_li/adjust_iceshelf_geometry.py index 59d976b90..f0b6ad359 100644 --- a/landice/mesh_tools_li/adjust_iceshelf_geometry.py +++ b/landice/mesh_tools_li/adjust_iceshelf_geometry.py @@ -31,7 +31,7 @@ nSmoothing = 3 # number of rounds of smoothing to apply. 3 was the best match at 8km to 30 years of thickness evolution on Ross and FRIS. Set to 0 to disable -elevationDrop = 5.0 # height in meters to drop non-grounded marine bed elevation +elevationDrop = 20.0 # height in meters to drop non-grounded marine bed elevation # ------------------------ diff --git a/landice/mesh_tools_li/bulldoze_missing_troughs.py b/landice/mesh_tools_li/bulldoze_missing_troughs.py index 78f0a88d2..152f1134b 100644 --- a/landice/mesh_tools_li/bulldoze_missing_troughs.py +++ b/landice/mesh_tools_li/bulldoze_missing_troughs.py @@ -117,8 +117,8 @@ def smoothTrough(WCT, p1, p2, minWCT, maxWCT): # Define maximum and minimum water column thickness along tough centerline -maxWCT = 300.0 -minWCT = 50.0 +maxWCT = 400.0 +minWCT = 75.0 # Specify start and end x/y coordinates for each trough to be bulldozed WCTnew = WCT.copy() From d94220a1f2ab700ff25863e22210916d67984688 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 3 Jan 2024 12:28:05 -0800 Subject: [PATCH 080/169] Add iceMask to mpas-to-mpas interpolation --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index 4d06cbe63..93c79a5fe 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -702,6 +702,7 @@ def vertical_interp_MPAS_grid(mpas_grid_input_layers, destVertCoord, input_layer fieldInfo['stiffnessFactor'] = {'InputName':'stiffnessFactor', 'scalefactor':1.0, 'offset':0.0, 'gridType':'cell', 'vertDim':False} fieldInfo['effectivePressure'] = {'InputName':'effectivePressure', 'scalefactor':1.0, 'offset':0.0, 'gridType':'cell', 'vertDim':False} + fieldInfo['iceMask'] = {'InputName':'iceMask', 'scalefactor':1.0, 'offset':0.0, 'gridType':'cell', 'vertDim':False} # Used by Trevor # fieldInfo['sfcMassBalUncertainty'] = {'InputName':'smb_std_vector', 'scalefactor':910.0/(3600.0*24.0*365.0)/1000.0, 'offset':0.0, 'gridType':'cell', 'vertDim':False} From 28428abc7a34d9f3bb5cc06f93fe3f99d4028999 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 4 Jan 2024 11:29:07 +0100 Subject: [PATCH 081/169] Fix docs for coastline alteration --- .../docs/ocean/coastline_alteration.rst | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/conda_package/docs/ocean/coastline_alteration.rst b/conda_package/docs/ocean/coastline_alteration.rst index 5a9385ecb..785b85865 100644 --- a/conda_package/docs/ocean/coastline_alteration.rst +++ b/conda_package/docs/ocean/coastline_alteration.rst @@ -27,10 +27,13 @@ An example of the typical workflow that uses this function would be: import xarray from geometric_features import GeometricFeatures - from mpas_tools.mesh import conversion + from mpas_tools.cime.constants import constants + from mpas_tools.mesh import mask from mpas_tools.ocean.coastline_alteration import add_critical_land_blockages + earthRadius = constants['SHR_CONST_REARTH'] + # an object used to read feature collections from the geometric_features # package gf = GeometricFeatures() @@ -43,7 +46,9 @@ An example of the typical workflow that uses this function would be: dsBaseMesh = xarray.open_dataset('base_mesh.nc') # create a mask on the base mesh that is ones for land or grounded ice and # zeros for ocean - dsLandMask = conversion.mask(dsBaseMesh, fcMask=fcLandCoverage) + dsLandMask = mask.compute_mpas_region_masks(dsBaseMesh, + fcMask=fcLandCoverage, + maskTypes=('cell',)) # get a collection of features from geometric_features that are meant for # use as critical land blockages @@ -51,7 +56,9 @@ An example of the typical workflow that uses this function would be: tags=['Critical_Land_Blockage']) # make masks from the transects - dsCritBlockMask = conversion.mask(dsBaseMesh, fcMask=fcCritBlockages) + dsCritBlockMask = mask.compute_mpas_transect_masks(dsBaseMesh, + fcMask=fcCritBlockages, + earthRadius=earthRadius) # add the masks to the "land" mask dsLandMask = add_critical_land_blockages(dsLandMask, dsCritBlockMask) @@ -83,7 +90,7 @@ Here is an example workflow that removes land-locked cells: import xarray from geometric_features import GeometricFeatures - from mpas_tools.mesh import conversion + from mpas_tools.mesh import mask from mpas_tools.ocean.coastline_alteration import \ add_land_locked_cells_to_mask @@ -100,7 +107,9 @@ Here is an example workflow that removes land-locked cells: dsBaseMesh = xarray.open_dataset('base_mesh.nc') # create a mask on the base mesh that is ones for land or grounded ice and # zeros for ocean - dsLandMask = conversion.mask(dsBaseMesh, fcMask=fcLandCoverage) + dsLandMask = mask.compute_mpas_region_masks(dsBaseMesh, + fcMask=fcLandCoverage, + maskTypes=('cell',)) # Find ocean cells that are land-locked, and alter the cell mask so that # they are counted as land cells @@ -128,9 +137,12 @@ An example workflow that includes transect-widening is: import xarray from geometric_features import GeometricFeatures - from mpas_tools.mesh import conversion + from mpas_tools.cime.constants import constants + from mpas_tools.mesh import mask from mpas_tools.ocean.coastline_alteration import widen_transect_edge_masks + earthRadius = constants['SHR_CONST_REARTH'] + # an object used to read feature collections from the geometric_features # package gf = GeometricFeatures() @@ -143,7 +155,9 @@ An example workflow that includes transect-widening is: tags=['Critical_Passage']) # create masks from the transects - dsCritPassMask = conversion.mask(dsBaseMesh, fcMask=fcCritPassages) + dsCritPassMask = mask.compute_mpas_transect_masks(dsBaseMesh, + fcMask=fcCritPassages, + earthRadius=earthRadius) # Alter critical passages to be at least two cells wide, to avoid sea ice # blockage. From a3f0c2223ae968953766cae5f79d065d223a3c60 Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Thu, 4 Jan 2024 07:41:47 -0800 Subject: [PATCH 082/169] Add missing comma preventing tune_ismip6_melt_deltat.py from running --- landice/mesh_tools_li/tune_ismip6_melt_deltat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/tune_ismip6_melt_deltat.py b/landice/mesh_tools_li/tune_ismip6_melt_deltat.py index fcee6129e..099e8074e 100644 --- a/landice/mesh_tools_li/tune_ismip6_melt_deltat.py +++ b/landice/mesh_tools_li/tune_ismip6_melt_deltat.py @@ -39,7 +39,7 @@ # Increments of 0.05 seems than fine enough to get a smooth function for interpolating, but use larger increments # when testing for faster execution. #dTs = np.arange(-1.5, 2.0, 0.25) # MeanAnt - coarse spacing for rapid testing -dTs = np.arange(-1.0 1.5, 0.05) # MeanAnt - fine spacing for accurate calculation +dTs = np.arange(-1.0, 1.5, 0.05) # MeanAnt - fine spacing for accurate calculation #dTs = np.arange(-1.5, 0.0, 0.05) # PIGL # ----------------------- From ba7dd43982c40a6b9b05548d908ef9c19865c1a0 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 8 Jan 2024 06:13:18 -0800 Subject: [PATCH 083/169] Limit lon to -180 to 180 in lon-lat region masks This is needed for grids where longitude is from 0 to 360 and it is inconvenient to modify the grid before passing it to the command-line tool. --- conda_package/mpas_tools/mesh/mask.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conda_package/mpas_tools/mesh/mask.py b/conda_package/mpas_tools/mesh/mask.py index 036bd5bb2..838a5a5d4 100644 --- a/conda_package/mpas_tools/mesh/mask.py +++ b/conda_package/mpas_tools/mesh/mask.py @@ -522,6 +522,9 @@ def compute_lon_lat_region_masks(lon, lat, fcMask, logger=None, pool=None, dsMasks = xr.Dataset() + # make sure lon is between -180 and 180 + lon = numpy.mod(lon + 180., 360.) - 180. + Lon, Lat = numpy.meshgrid(lon, lat) shape = Lon.shape From b0e614726526305d44e62fa83111ee6e750c322a Mon Sep 17 00:00:00 2001 From: Matthew Hoffman Date: Wed, 17 Jan 2024 20:13:16 -0700 Subject: [PATCH 084/169] Update comments and variable name to be more descriptive --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index a0ddb7c2c..1df966dd1 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -71,8 +71,8 @@ wfile.close() #---------------------------- - # convert to sparse matrix format - wt_csr = scipy.sparse.coo_array((S, (row - 1, col - 1)), shape=(n_b, n_a)).tocsr() + # convert to SciPy Compressed Sparse Row (CSR) matrix format + weights_csr = scipy.sparse.coo_array((S, (row - 1, col - 1)), shape=(n_b, n_a)).tocsr() print('') # make a space in stdout before further output @@ -88,8 +88,11 @@ def ESMF_interp(sourceField): # Interpolates from the sourceField to the destinationField using ESMF weights destinationField = np.zeros(xCell.shape) # fields on cells only try: + # Convert the source field into the SciPy Compressed Sparse Row matrix format + # This needs some reshaping to get the matching dimensions source_csr = scipy.sparse.csr_matrix(sourceField.flatten()[:, np.newaxis]) - destinationField = wt_csr.dot(source_csr).toarray().squeeze() + # Use SciPy CSR dot product - much faster than iterating over elements of the full matrix + destinationField = weights_csr.dot(source_csr).toarray().squeeze() # For conserve remapping, need to normalize by destination area fraction # It should be safe to do this for other methods ind = np.where(dst_frac > 0.0)[0] From e9e8d09afad6e8449fbec268abdd37a1ad89c8ec Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 20 Jan 2024 12:53:08 +0100 Subject: [PATCH 085/169] Update to v0.30.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 24b674684..09cdc1330 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -42,6 +42,7 @@ Documentation On GitHub `v0.27.0`_ `0.27.0`_ `v0.28.0`_ `0.28.0`_ `v0.29.0`_ `0.29.0`_ +`v0.30.0`_ `0.30.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -116,3 +117,5 @@ Documentation On GitHub .. _`0.28.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.28.0 .. _`v0.29.0`: ../0.29.0/index.html .. _`0.29.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.29.0 +.. _`v0.30.0`: ../0.30.0/index.html +.. _`0.30.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.30.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index fe5c126e0..a971668ab 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 29, 0) +__version_info__ = (0, 30, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 90da4f40f..5858a008a 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.29.0" %} +{% set version = "0.30.0" %} package: name: {{ name|lower }} From 5758643cdea6bcb8d3b97fb3bb388ed98eb9bbb3 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Thu, 25 Jan 2024 15:48:28 -0500 Subject: [PATCH 086/169] jig-to-nc: 3rd value for POINT not coord in 2d see https://github.com/dengwirda/jigsaw/wiki/MSH-File-Format#point-segment --- conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py index 96126f0ea..503802553 100644 --- a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py +++ b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py @@ -50,7 +50,11 @@ def jigsaw_to_netcdf(msh_filename, output_name, on_sphere, sphere_radius=None): # Create cell variables and sphere_radius xCell_full = msh['POINT'][:, 0] yCell_full = msh['POINT'][:, 1] - zCell_full = msh['POINT'][:, 2] + zCell_full = [] + if msh['NDIMS'] == 3: + zCell_full = msh['POINT'][:, 2] + else: + zCell_full = np.zeros(yCell_full.shape) for cells in [xCell_full, yCell_full, zCell_full]: assert cells.shape[0] == nCells, 'Number of anticipated nodes is' \ ' not correct!' From 9480b338cf13486a9ed46fa762c7c9907327a772 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Sun, 28 Jan 2024 13:34:21 -0500 Subject: [PATCH 087/169] jigsaw_to_netcdf: only support 2d and 3d meshes Co-authored-by: Xylar Asay-Davis --- conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py index 503802553..a898e236f 100644 --- a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py +++ b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py @@ -51,10 +51,12 @@ def jigsaw_to_netcdf(msh_filename, output_name, on_sphere, sphere_radius=None): xCell_full = msh['POINT'][:, 0] yCell_full = msh['POINT'][:, 1] zCell_full = [] - if msh['NDIMS'] == 3: + if msh['NDIMS'] == 2: + zCell_full = np.zeros(yCell_full.shape) + elif msh['NDIMS'] == 3: zCell_full = msh['POINT'][:, 2] else: - zCell_full = np.zeros(yCell_full.shape) + raise ValueError(f'Unexpected mesh NDIMS: {msh["NDIMS"]}') for cells in [xCell_full, yCell_full, zCell_full]: assert cells.shape[0] == nCells, 'Number of anticipated nodes is' \ ' not correct!' From af6730a02e05363b8bd56e82471c8a4dfa76a44b Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Sun, 28 Jan 2024 13:41:55 -0500 Subject: [PATCH 088/169] setup: always copy subdirs ocean, landice,... If changes are made to the scripts in these directories running `python -m pip install -e conda_package` will not update them. This change removes the directory existence check so that they are always copied. copytree will not overwrite, so we need to delete the existing dir first. --- conda_package/setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/conda_package/setup.py b/conda_package/setup.py index 99fc36643..66a476a93 100755 --- a/conda_package/setup.py +++ b/conda_package/setup.py @@ -34,8 +34,10 @@ os.chdir(here) for path in ['ocean', 'landice', 'visualization', 'mesh_tools']: - if not os.path.exists(path): - shutil.copytree('../{}'.format(path), './{}'.format(path)) + destPath = './{}'.format(path) + if os.path.exists(destPath): + shutil.rmtree(destPath) + shutil.copytree('../{}'.format(path), destPath) setup(name='mpas_tools', version=version, From 7c323aac6e6b1b1df987196e3b83a6cb91d6c8ca Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 30 Jan 2024 16:28:24 -0800 Subject: [PATCH 089/169] Ignore NaNs when checking for negative basalHeatFlux Ignore NaNs when checking for negative basalHeatFlux by using np.nanmin() instead of just min(). This will not throw an assertion error when NaNs are present in the interpolated basalHeatFlux field. NaNs in datasets are a separate issue from sign conventions, so will be dealt with separately. --- landice/mesh_tools_li/interpolate_to_mpasli_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py index 8165c412f..2b83a86b9 100755 --- a/landice/mesh_tools_li/interpolate_to_mpasli_grid.py +++ b/landice/mesh_tools_li/interpolate_to_mpasli_grid.py @@ -774,7 +774,7 @@ def vertical_interp_MPAS_grid(mpas_grid_input_layers, destVertCoord, input_layer # basalHeatFlux must be non-negative if MPASfieldName == 'basalHeatFlux': - assert MPASfield.min() >= 0.0, 'basalHeatFlux contains negative values! This is likely due to the ' \ + assert np.nanmin(MPASfield) >= 0.0, 'basalHeatFlux contains negative values! This is likely due to the ' \ 'conventions used in the input file, rather than bad data. Ensure ' \ 'non-negative values before interpolating.' From da0d533b9db78b9f307f8c92adac92ac750d29f6 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Fri, 2 Feb 2024 08:53:56 -0500 Subject: [PATCH 090/169] jigsaw_to_netcdf: more helpful error message --- conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py index a898e236f..062f28d78 100644 --- a/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py +++ b/conda_package/mpas_tools/mesh/creation/jigsaw_to_netcdf.py @@ -56,7 +56,8 @@ def jigsaw_to_netcdf(msh_filename, output_name, on_sphere, sphere_radius=None): elif msh['NDIMS'] == 3: zCell_full = msh['POINT'][:, 2] else: - raise ValueError(f'Unexpected mesh NDIMS: {msh["NDIMS"]}') + raise ValueError("NDIMS must be 2 or 3; input mesh has NDIMS={}" + .format(msh['NDIMS'])) for cells in [xCell_full, yCell_full, zCell_full]: assert cells.shape[0] == nCells, 'Number of anticipated nodes is' \ ' not correct!' From d1d8b8309b0f6fe87d53bee00c2193f8ef170939 Mon Sep 17 00:00:00 2001 From: Jacob Kim Date: Thu, 15 Feb 2024 11:39:36 +0900 Subject: [PATCH 091/169] Typo: Fix 'distination' to 'destination' --- mesh_tools/grid_rotate/grid_rotate.f90 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesh_tools/grid_rotate/grid_rotate.f90 b/mesh_tools/grid_rotate/grid_rotate.f90 index e2d048137..650427496 100644 --- a/mesh_tools/grid_rotate/grid_rotate.f90 +++ b/mesh_tools/grid_rotate/grid_rotate.f90 @@ -95,7 +95,7 @@ subroutine main() end if if(trim(newFilename) == "") then - write(0,*) "Error: no distination file was specified" + write(0,*) "Error: no destination file was specified" return end if From e6958156b6be4c40a9b2a8ac90be1476f36589e1 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 15 Feb 2024 03:24:23 -0600 Subject: [PATCH 092/169] Update to v0.31.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 09cdc1330..bc24806e1 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -43,6 +43,7 @@ Documentation On GitHub `v0.28.0`_ `0.28.0`_ `v0.29.0`_ `0.29.0`_ `v0.30.0`_ `0.30.0`_ +`v0.31.0`_ `0.31.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -119,3 +120,5 @@ Documentation On GitHub .. _`0.29.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.29.0 .. _`v0.30.0`: ../0.30.0/index.html .. _`0.30.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.30.0 +.. _`v0.31.0`: ../0.31.0/index.html +.. _`0.31.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.31.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index a971668ab..6b6c537c4 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 30, 0) +__version_info__ = (0, 31, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index 5858a008a..bd09ec360 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.30.0" %} +{% set version = "0.31.0" %} package: name: {{ name|lower }} From 5f45f095403d6cdbb31494476d7f238fb176d1c0 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 15 Feb 2024 08:31:48 +0100 Subject: [PATCH 093/169] Update CI with conda-forge config --- azure-pipelines.yml | 4 +-- ...l => linux_64_python3.10.____cpython.yaml} | 2 +- ...l => linux_64_python3.11.____cpython.yaml} | 2 +- .../ci/linux_64_python3.12.____cpython.yaml | 33 +++++++++++++++++ .../ci/linux_64_python3.8.____cpython.yaml | 33 +++++++++++++++++ ...ml => linux_64_python3.9.____cpython.yaml} | 2 +- ...aml => osx_64_python3.10.____cpython.yaml} | 6 ++-- ...aml => osx_64_python3.11.____cpython.yaml} | 6 ++-- .../ci/osx_64_python3.12.____cpython.yaml | 35 +++++++++++++++++++ .../ci/osx_64_python3.8.____cpython.yaml | 35 +++++++++++++++++++ ...yaml => osx_64_python3.9.____cpython.yaml} | 6 ++-- 11 files changed, 150 insertions(+), 14 deletions(-) rename conda_package/ci/{linux_python3.10.yaml => linux_64_python3.10.____cpython.yaml} (98%) rename conda_package/ci/{linux_python3.11.yaml => linux_64_python3.11.____cpython.yaml} (98%) create mode 100644 conda_package/ci/linux_64_python3.12.____cpython.yaml create mode 100644 conda_package/ci/linux_64_python3.8.____cpython.yaml rename conda_package/ci/{linux_python3.9.yaml => linux_64_python3.9.____cpython.yaml} (98%) rename conda_package/ci/{osx_python3.10.yaml => osx_64_python3.10.____cpython.yaml} (95%) rename conda_package/ci/{osx_python3.11.yaml => osx_64_python3.11.____cpython.yaml} (95%) create mode 100644 conda_package/ci/osx_64_python3.12.____cpython.yaml create mode 100644 conda_package/ci/osx_64_python3.8.____cpython.yaml rename conda_package/ci/{osx_python3.9.yaml => osx_64_python3.9.____cpython.yaml} (95%) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4dac520ed..788358aba 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,7 +38,7 @@ jobs: - bash: | eval "$(conda shell.bash hook)" conda activate build - conda build -m "conda_package/ci/linux_python${PYTHON_VERSION}.yaml" "conda_package/recipe" + conda build -m "conda_package/ci/linux_64_python${PYTHON_VERSION}.____cpython.yaml" "conda_package/recipe" displayName: Build MPAS-Tools - bash: | @@ -158,7 +158,7 @@ jobs: - bash: | eval "$(conda shell.bash hook)" conda activate build - conda build -m "conda_package/ci/osx_python${PYTHON_VERSION}.yaml" "conda_package/recipe" + conda build -m "conda_package/ci/osx_64_python${PYTHON_VERSION}.____cpython.yaml" "conda_package/recipe" displayName: Build MPAS-Tools - bash: | diff --git a/conda_package/ci/linux_python3.10.yaml b/conda_package/ci/linux_64_python3.10.____cpython.yaml similarity index 98% rename from conda_package/ci/linux_python3.10.yaml rename to conda_package/ci/linux_64_python3.10.____cpython.yaml index 2fa257cee..de2c8f384 100644 --- a/conda_package/ci/linux_python3.10.yaml +++ b/conda_package/ci/linux_64_python3.10.____cpython.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/linux_python3.11.yaml b/conda_package/ci/linux_64_python3.11.____cpython.yaml similarity index 98% rename from conda_package/ci/linux_python3.11.yaml rename to conda_package/ci/linux_64_python3.11.____cpython.yaml index bb5761d64..b973d3ff8 100644 --- a/conda_package/ci/linux_python3.11.yaml +++ b/conda_package/ci/linux_64_python3.11.____cpython.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/linux_64_python3.12.____cpython.yaml b/conda_package/ci/linux_64_python3.12.____cpython.yaml new file mode 100644 index 000000000..4b8de5dc8 --- /dev/null +++ b/conda_package/ci/linux_64_python3.12.____cpython.yaml @@ -0,0 +1,33 @@ +cdt_name: +- cos6 +channel_sources: +- conda-forge +channel_targets: +- conda-forge main +cxx_compiler: +- gxx +cxx_compiler_version: +- '12' +docker_image: +- quay.io/condaforge/linux-anvil-cos7-x86_64 +fortran_compiler: +- gfortran +fortran_compiler_version: +- '12' +hdf5: +- 1.14.3 +libnetcdf: +- 4.9.2 +netcdf_fortran: +- '4.6' +pin_run_as_build: + python: + min_pin: x.x + max_pin: x.x +python: +- 3.12.* *_cpython +target_platform: +- linux-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/linux_64_python3.8.____cpython.yaml b/conda_package/ci/linux_64_python3.8.____cpython.yaml new file mode 100644 index 000000000..69ab4bef7 --- /dev/null +++ b/conda_package/ci/linux_64_python3.8.____cpython.yaml @@ -0,0 +1,33 @@ +cdt_name: +- cos6 +channel_sources: +- conda-forge +channel_targets: +- conda-forge main +cxx_compiler: +- gxx +cxx_compiler_version: +- '12' +docker_image: +- quay.io/condaforge/linux-anvil-cos7-x86_64 +fortran_compiler: +- gfortran +fortran_compiler_version: +- '12' +hdf5: +- 1.14.3 +libnetcdf: +- 4.9.2 +netcdf_fortran: +- '4.6' +pin_run_as_build: + python: + min_pin: x.x + max_pin: x.x +python: +- 3.8.* *_cpython +target_platform: +- linux-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/linux_python3.9.yaml b/conda_package/ci/linux_64_python3.9.____cpython.yaml similarity index 98% rename from conda_package/ci/linux_python3.9.yaml rename to conda_package/ci/linux_64_python3.9.____cpython.yaml index 03c939c7a..e7cff6d40 100644 --- a/conda_package/ci/linux_python3.9.yaml +++ b/conda_package/ci/linux_64_python3.9.____cpython.yaml @@ -15,7 +15,7 @@ fortran_compiler: fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 netcdf_fortran: diff --git a/conda_package/ci/osx_python3.10.yaml b/conda_package/ci/osx_64_python3.10.____cpython.yaml similarity index 95% rename from conda_package/ci/osx_python3.10.yaml rename to conda_package/ci/osx_64_python3.10.____cpython.yaml index 37e52bc37..ade8240bb 100644 --- a/conda_package/ci/osx_python3.10.yaml +++ b/conda_package/ci/osx_64_python3.10.____cpython.yaml @@ -7,17 +7,17 @@ channel_targets: cxx_compiler: - clangxx cxx_compiler_version: -- '15' +- '16' fortran_compiler: - gfortran fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 llvm_openmp: -- '15' +- '16' macos_machine: - x86_64-apple-darwin13.4.0 netcdf_fortran: diff --git a/conda_package/ci/osx_python3.11.yaml b/conda_package/ci/osx_64_python3.11.____cpython.yaml similarity index 95% rename from conda_package/ci/osx_python3.11.yaml rename to conda_package/ci/osx_64_python3.11.____cpython.yaml index a1ce0cbb3..248ca3347 100644 --- a/conda_package/ci/osx_python3.11.yaml +++ b/conda_package/ci/osx_64_python3.11.____cpython.yaml @@ -7,17 +7,17 @@ channel_targets: cxx_compiler: - clangxx cxx_compiler_version: -- '15' +- '16' fortran_compiler: - gfortran fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 llvm_openmp: -- '15' +- '16' macos_machine: - x86_64-apple-darwin13.4.0 netcdf_fortran: diff --git a/conda_package/ci/osx_64_python3.12.____cpython.yaml b/conda_package/ci/osx_64_python3.12.____cpython.yaml new file mode 100644 index 000000000..f4b5c65a5 --- /dev/null +++ b/conda_package/ci/osx_64_python3.12.____cpython.yaml @@ -0,0 +1,35 @@ +MACOSX_DEPLOYMENT_TARGET: +- '10.9' +channel_sources: +- conda-forge +channel_targets: +- conda-forge main +cxx_compiler: +- clangxx +cxx_compiler_version: +- '16' +fortran_compiler: +- gfortran +fortran_compiler_version: +- '12' +hdf5: +- 1.14.3 +libnetcdf: +- 4.9.2 +llvm_openmp: +- '16' +macos_machine: +- x86_64-apple-darwin13.4.0 +netcdf_fortran: +- '4.6' +pin_run_as_build: + python: + min_pin: x.x + max_pin: x.x +python: +- 3.12.* *_cpython +target_platform: +- osx-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/osx_64_python3.8.____cpython.yaml b/conda_package/ci/osx_64_python3.8.____cpython.yaml new file mode 100644 index 000000000..0798779bb --- /dev/null +++ b/conda_package/ci/osx_64_python3.8.____cpython.yaml @@ -0,0 +1,35 @@ +MACOSX_DEPLOYMENT_TARGET: +- '10.9' +channel_sources: +- conda-forge +channel_targets: +- conda-forge main +cxx_compiler: +- clangxx +cxx_compiler_version: +- '16' +fortran_compiler: +- gfortran +fortran_compiler_version: +- '12' +hdf5: +- 1.14.3 +libnetcdf: +- 4.9.2 +llvm_openmp: +- '16' +macos_machine: +- x86_64-apple-darwin13.4.0 +netcdf_fortran: +- '4.6' +pin_run_as_build: + python: + min_pin: x.x + max_pin: x.x +python: +- 3.8.* *_cpython +target_platform: +- osx-64 +zip_keys: +- - cxx_compiler_version + - fortran_compiler_version diff --git a/conda_package/ci/osx_python3.9.yaml b/conda_package/ci/osx_64_python3.9.____cpython.yaml similarity index 95% rename from conda_package/ci/osx_python3.9.yaml rename to conda_package/ci/osx_64_python3.9.____cpython.yaml index 7262ae4f0..8e1712f42 100644 --- a/conda_package/ci/osx_python3.9.yaml +++ b/conda_package/ci/osx_64_python3.9.____cpython.yaml @@ -7,17 +7,17 @@ channel_targets: cxx_compiler: - clangxx cxx_compiler_version: -- '15' +- '16' fortran_compiler: - gfortran fortran_compiler_version: - '12' hdf5: -- 1.14.2 +- 1.14.3 libnetcdf: - 4.9.2 llvm_openmp: -- '15' +- '16' macos_machine: - x86_64-apple-darwin13.4.0 netcdf_fortran: From a9a6722db5859b2e3d2f581fa599ea394d6af980 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 14 Feb 2024 22:31:56 +0100 Subject: [PATCH 094/169] Make `meshDensity` an optional variable in MpasCellCuller.x --- .../mpas_cell_culler.cpp | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index e4168333a..4ca864fb5 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -1035,6 +1035,8 @@ int mapAndOutputCellFields( const string inputFilename, const string outputPath, int *tmp_arr_old, *nEdgesOnCellOld, *nEdgesOnCellNew; int *tmp_arr_new; + bool hasMeshDensity; + tmp_arr_old = new int[nCells*maxEdges]; nEdgesOnCellOld = new int[nCells]; nEdgesOnCellNew = new int[nCellsNew]; @@ -1175,20 +1177,30 @@ int mapAndOutputCellFields( const string inputFilename, const string outputPath, // Map meshDensity meshDensityOld = new double[nCells]; - meshDensityNew = new double[nCellsNew]; - ncutil::get_var(inputFilename, "meshDensity", meshDensityOld); + try { + ncutil::get_var(inputFilename, "meshDensity", meshDensityOld); + hasMeshDensity = true; + } catch (...) { + // allow errors for optional vars. not found + hasMeshDensity = false; + } - for(int iCell = 0; iCell < nCells; iCell++){ - if(cellMap.at(iCell) != -1){ - meshDensityNew[cellMap.at(iCell)] = meshDensityOld[iCell]; + if(hasMeshDensity) { + meshDensityNew = new double[nCellsNew]; + for(int iCell = 0; iCell < nCells; iCell++){ + if(cellMap.at(iCell) != -1){ + meshDensityNew[cellMap.at(iCell)] = meshDensityOld[iCell]; + } } - } - ncutil::def_var(outputFilename, "meshDensity", - NC_DOUBLE, "mesh density distribution", {"nCells"}); + ncutil::def_var(outputFilename, "meshDensity", + NC_DOUBLE, "mesh density distribution", {"nCells"}); - ncutil::put_var(outputFilename, "meshDensity", &meshDensityNew[0]); + ncutil::put_var(outputFilename, "meshDensity", &meshDensityNew[0]); + delete[] meshDensityNew; + } + delete[] meshDensityOld; return 0; }/*}}}*/ From 1724715cebc9d95b17d547d480ec6c7571dff169 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 15 Feb 2024 02:05:59 -0600 Subject: [PATCH 095/169] Make `weightsOnEdge` an optional variable in MpasCellCuller.x --- .../mpas_cell_culler.cpp | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index 4ca864fb5..0850b3ec6 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -1232,6 +1232,8 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen double *dcEdgeOld, *dcEdgeNew; double *angleEdgeOld, *angleEdgeNew; + bool hasWeightsOnEdge; + // Need to map cellsOnEdge and verticesOnEdge cellsOnEdgeOld = new int[nEdges*2]; cellsOnEdgeNew = new int[nEdgesNew*2]; @@ -1332,7 +1334,13 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen ncutil::get_var(inputFilename, "nEdgesOnEdge", nEdgesOnEdgeOld); ncutil::get_var(inputFilename, "edgesOnEdge", edgesOnEdgeOld); - ncutil::get_var(inputFilename, "weightsOnEdge", weightsOnEdgeOld); + try{ + ncutil::get_var(inputFilename, "weightsOnEdge", weightsOnEdgeOld); + hasWeightsOnEdge = true; + } catch (...) { + // allow errors for optional vars. not found + hasWeightsOnEdge = false; + } for(int iEdge = 0; iEdge < nEdges; iEdge++){ int edgeCount = 0; @@ -1356,24 +1364,32 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen if(eoe != -1 && eoe < edgeMap.size()){ edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = edgeMap.at(eoe) + 1; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = - weightsOnEdgeOld[iEdge*maxEdges*2 + j]; + if(hasWeightsOnEdge) { + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = + weightsOnEdgeOld[iEdge*maxEdges*2 + j]; + } edgeCount++; } else { edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + if(hasWeightsOnEdge) { + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } } } } else if ( cell1 == -1 || cell2 == -1){ for(int j = 0; j < nEdgesOnEdgeOld[iEdge]; j++){ edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + if(hasWeightsOnEdge) { + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } } } for(int j = edgeCount; j < maxEdges2New; j++){ edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + if(hasWeightsOnEdge) { + weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; + } } nEdgesOnEdgeNew[edgeMap.at(iEdge)] = edgeCount; } @@ -1387,10 +1403,12 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen ncutil::put_var(outputFilename, "nEdgesOnEdge", &nEdgesOnEdgeNew[0]); ncutil::put_var(outputFilename, "edgesOnEdge", &edgesOnEdgeNew[0]); - ncutil::def_var(outputFilename, "weightsOnEdge", - NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); + if(hasWeightsOnEdge) { + ncutil::def_var(outputFilename, "weightsOnEdge", + NC_DOUBLE, "tangential flux reconstruction weights", {"nEdges", "maxEdges2"}); - ncutil::put_var(outputFilename, "weightsOnEdge", &weightsOnEdgeNew[0]); + ncutil::put_var(outputFilename, "weightsOnEdge", &weightsOnEdgeNew[0]); + } delete[] nEdgesOnEdgeOld; delete[] cellsOnEdgeOld; From 20d1434a36e35770547365de395cd9e42d4e02c3 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 15 Feb 2024 03:44:58 -0600 Subject: [PATCH 096/169] Make `angleEdge` an optional variable in MpasCellCuller.x --- .../mpas_cell_culler.cpp | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp index 0850b3ec6..3ac3859a1 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/mpas_cell_culler.cpp @@ -1232,7 +1232,7 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen double *dcEdgeOld, *dcEdgeNew; double *angleEdgeOld, *angleEdgeNew; - bool hasWeightsOnEdge; + bool hasWeightsOnEdge, hasAngleEdge; // Need to map cellsOnEdge and verticesOnEdge cellsOnEdgeOld = new int[nEdges*2]; @@ -1387,7 +1387,7 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen for(int j = edgeCount; j < maxEdges2New; j++){ edgesOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; - if(hasWeightsOnEdge) { + if(hasWeightsOnEdge){ weightsOnEdgeNew[edgeMap.at(iEdge)*maxEdges2New + j] = 0; } } @@ -1427,13 +1427,22 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen ncutil::get_var(inputFilename, "dvEdge", dvEdgeOld); ncutil::get_var(inputFilename, "dcEdge", dcEdgeOld); - ncutil::get_var(inputFilename, "angleEdge", angleEdgeOld); + + try{ + ncutil::get_var(inputFilename, "angleEdge", angleEdgeOld); + hasAngleEdge = true; + } catch (...) { + // allow errors for optional vars. not found + hasAngleEdge = false; + } for(int iEdge = 0; iEdge < nEdges; iEdge++){ if(edgeMap.at(iEdge) != -1){ dvEdgeNew[edgeMap.at(iEdge)] = dvEdgeOld[iEdge]; dcEdgeNew[edgeMap.at(iEdge)] = dcEdgeOld[iEdge]; - angleEdgeNew[edgeMap.at(iEdge)] = angleEdgeOld[iEdge]; + if(hasAngleEdge) { + angleEdgeNew[edgeMap.at(iEdge)] = angleEdgeOld[iEdge]; + } } } @@ -1446,7 +1455,9 @@ int mapAndOutputEdgeFields( const string inputFilename, const string outputFilen ncutil::put_var(outputFilename, "dvEdge", &dvEdgeNew[0]); ncutil::put_var(outputFilename, "dcEdge", &dcEdgeNew[0]); - ncutil::put_var(outputFilename, "angleEdge", &angleEdgeNew[0]); + if(hasAngleEdge) { + ncutil::put_var(outputFilename, "angleEdge", &angleEdgeNew[0]); + } delete[] dvEdgeOld; delete[] dvEdgeNew; From 4c83a15ae45b4280dcf4121e9ed4a135f7960d4b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 15 Feb 2024 04:43:19 -0600 Subject: [PATCH 097/169] Change xarray `dims` --> `sizes` Support for accessing `dims` as a dictionary is deprecated. --- .../mpas_tools/mesh/creation/sort_mesh.py | 40 +++++++++---------- conda_package/mpas_tools/ocean/moc.py | 12 +++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/conda_package/mpas_tools/mesh/creation/sort_mesh.py b/conda_package/mpas_tools/mesh/creation/sort_mesh.py index 2eb75ad79..9b7ec3b9f 100644 --- a/conda_package/mpas_tools/mesh/creation/sort_mesh.py +++ b/conda_package/mpas_tools/mesh/creation/sort_mesh.py @@ -11,12 +11,12 @@ def sort_mesh(mesh): """ Sort cells, edges and duals in the mesh to improve cache-locality - + Parameters ---------- mesh : xarray.Dataset A dataset containing an MPAS mesh to sort - + Returns ------- mesh : xarray.Dataset @@ -24,9 +24,9 @@ def sort_mesh(mesh): """ # Authors: Darren Engwirda - ncells = mesh.dims["nCells"] - nedges = mesh.dims["nEdges"] - nduals = mesh.dims["nVertices"] + ncells = mesh.sizes["nCells"] + nedges = mesh.sizes["nEdges"] + nduals = mesh.sizes["nVertices"] cell_fwd = np.arange(0, ncells) + 1 cell_rev = np.arange(0, ncells) + 1 @@ -110,8 +110,8 @@ def sort_mesh(mesh): mesh["indexToEdgeID"][:] = np.arange(nedges) + 1 return mesh - - + + def main(): parser = argparse.ArgumentParser( description=__doc__, @@ -135,7 +135,7 @@ def main(): args.sort_file), "graph.info"), "w") as fptr: cellsOnCell = mesh["cellsOnCell"].values - ncells = mesh.dims["nCells"] + ncells = mesh.sizes["nCells"] nedges = np.count_nonzero(cellsOnCell) // 2 fptr.write(f"{ncells} {nedges}\n") @@ -152,15 +152,15 @@ def main(): def _sort_fwd(data, fwd): """ Apply a forward permutation to a mesh array - + Parameters ---------- data : array-like An MPAS mesh array to permute - + fwd : numpy.ndarray - An array of integers defining the permutation - + An array of integers defining the permutation + Returns ------- data : numpy.ndarray @@ -174,15 +174,15 @@ def _sort_fwd(data, fwd): def _sort_rev(data, rev): """ Apply a reverse permutation to a mesh array - + Parameters ---------- data : array-like An MPAS mesh array to permute - + rev : numpy.ndarray - An array of integers defining the permutation - + An array of integers defining the permutation + Returns ------- data : numpy.ndarray @@ -197,17 +197,17 @@ def _sort_rev(data, rev): def _cell_del2(mesh): """ Form cell-to-cell sparse adjacency graph - + Parameters ---------- mesh : xarray.Dataset A dataset containing an MPAS mesh to sort - + Returns ------- del2 : scipy.sparse.csr_matrix The cell-to-cell adjacency graph as a sparse matrix - """ + """ xvec = np.array([], dtype=np.int8) ivec = np.array([], dtype=np.int32) jvec = np.array([], dtype=np.int32) @@ -236,6 +236,6 @@ def _cell_del2(mesh): return csr_matrix((xvec, (ivec, jvec))) - + if (__name__ == "__main__"):\ main() diff --git a/conda_package/mpas_tools/ocean/moc.py b/conda_package/mpas_tools/ocean/moc.py index 6013da6ac..cb8cdc0a7 100755 --- a/conda_package/mpas_tools/ocean/moc.py +++ b/conda_package/mpas_tools/ocean/moc.py @@ -121,11 +121,11 @@ def _extract_southern_boundary(mesh, mocMask, latBuffer, logger): boundary. """ - nCells = mesh.dims['nCells'] - nEdges = mesh.dims['nEdges'] + nCells = mesh.sizes['nCells'] + nEdges = mesh.sizes['nEdges'] - nRegions = mocMask.dims['nRegions'] - assert(mocMask.dims['nCells'] == nCells) + nRegions = mocMask.sizes['nRegions'] + assert(mocMask.sizes['nCells'] == nCells) # convert to python zero-based indices cellsOnEdge = mesh.variables['cellsOnEdge'].values-1 @@ -241,8 +241,8 @@ def _add_transects_to_moc(mesh, mocMask, southernBoundaryEdges, nTransects = len(southernBoundaryEdges) - nEdges = mesh.dims['nEdges'] - nVertices = mesh.dims['nVertices'] + nEdges = mesh.sizes['nEdges'] + nVertices = mesh.sizes['nVertices'] maxEdgesInTransect = numpy.amax([len(southernBoundaryEdges[iTransect]) for iTransect in range(nTransects)]) From dc00d91d2a43cccfb8ea452791ab3c980481715e Mon Sep 17 00:00:00 2001 From: Michael Duda Date: Tue, 27 Feb 2024 12:55:14 -0700 Subject: [PATCH 098/169] grid_rotate: Work around problematic nf-config behavior in netCDF-Fortran 4.6.1 In netCDF-Fortran 4.6.1 (at least, as installed on NCAR's Derecho system), the `nf-config --flibs` output includes the netCDF-C library (-lnetcdf), but it does not include the library search path for this library, leading to compilation errors like ld: cannot find -lnetcdf ld: cannot find -lnetcdf As a workaround, this commit adds "-L$(shell nc-config --libdir)" to the definition of FCLIBS in the grid_rotate Makefile. --- mesh_tools/grid_rotate/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesh_tools/grid_rotate/Makefile b/mesh_tools/grid_rotate/Makefile index daf2d3af6..d6c66b306 100644 --- a/mesh_tools/grid_rotate/Makefile +++ b/mesh_tools/grid_rotate/Makefile @@ -1,7 +1,7 @@ FC = $(shell nf-config --fc) FFLAGS = -O3 FCINCLUDES = $(shell nf-config --fflags) -FCLIBS = $(shell nf-config --flibs) +FCLIBS = -L$(shell nc-config --libdir) $(shell nf-config --flibs) all: grid_rotate From 4223b426ec1b644c09ba0fe6cf56d1c3849ddb1a Mon Sep 17 00:00:00 2001 From: Michael Duda Date: Tue, 27 Feb 2024 12:57:01 -0700 Subject: [PATCH 099/169] grid_rotate: Address netCDF shared library issues through the use of rpath On NCAR's Derecho system, compiler wrappers generally handle the setting of rpath for shared libraries in compiled executables. However, the Fortran compiler used in the grid_rotate Makefile is set to the actual compiler used to build the netCDF library using `nf-config --fc`, thereby precluding the use of the compiler wrapper. This leads to runtime issues in locating the netCDF shared libraries, e.g., ./convert_mpas: error while loading shared libraries: libnetcdff.so.7: cannot open shared object file: No such file or directory As a workaround, the grid_rotate Makefile now tries to extract linker rpath flags from the output of `nf-config --flibs` and `nc-config --libs`. These flags are stored in the RPATH_FLAGS make variable, which is then appended to the FCLIBS variable. --- mesh_tools/grid_rotate/Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mesh_tools/grid_rotate/Makefile b/mesh_tools/grid_rotate/Makefile index d6c66b306..c73598a04 100644 --- a/mesh_tools/grid_rotate/Makefile +++ b/mesh_tools/grid_rotate/Makefile @@ -1,7 +1,9 @@ FC = $(shell nf-config --fc) FFLAGS = -O3 FCINCLUDES = $(shell nf-config --fflags) -FCLIBS = -L$(shell nc-config --libdir) $(shell nf-config --flibs) +RPATH_FLAGS = $(shell nf-config --flibs | grep -o -e '-L\S\+\( \|$$\)' | sed 's/^-L/-Wl,-rpath,/' | tr -d '\n') +RPATH_FLAGS += $(shell nc-config --libs | grep -o -e '-L\S\+\( \|$$\)' | sed 's/^-L/-Wl,-rpath,/' | tr -d '\n') +FCLIBS = -L$(shell nc-config --libdir) $(shell nf-config --flibs) $(RPATH_FLAGS) all: grid_rotate From 7a05637652e9c7334b46ea8feee891b1b889856e Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 13 Mar 2024 11:30:23 +0100 Subject: [PATCH 100/169] Add functions for mapping from culled to base mesh --- conda_package/mpas_tools/mesh/cull.py | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 conda_package/mpas_tools/mesh/cull.py diff --git a/conda_package/mpas_tools/mesh/cull.py b/conda_package/mpas_tools/mesh/cull.py new file mode 100644 index 000000000..0afdddbba --- /dev/null +++ b/conda_package/mpas_tools/mesh/cull.py @@ -0,0 +1,96 @@ +import numpy as np +import xarray as xr +from scipy.spatial import KDTree + +from mpas_tools.io import write_netcdf + + +def write_map_culled_to_base(base_mesh_filename, culled_mesh_filename, + out_filename, workers=-1): + """ + Write out a file with maps from cells, edges and vertices on a culled mesh + to the same elements on a base mesh. All elements in the culled mesh must + be in the base mesh. + + Parameters + ---------- + base_mesh_filename : str + A file with the horizontal MPAS mesh before culling + + culled_mesh_filename : str + A file with the culled horizonal MPAS mesh + + out_filename : str + A file to which the maps should be written. The dataset will include + ``mapCulledToBaseCell``, ``mapCulledToBaseEdge`` and + ``mapCulledToBaseVertex``, each of which contains the base mesh index + corresponding to the same element from the culled mesh. + + workers : int, optional + The number of threads to use to query basemesh elements. The default + is all available threads (``workers=-1``) + """ + ds_base = xr.open_dataset(base_mesh_filename) + ds_culled = xr.open_dataset(culled_mesh_filename) + ds_map_culled_to_base = map_culled_to_base(ds_base, ds_culled, + workers=workers) + write_netcdf(ds_map_culled_to_base, out_filename) + + +def map_culled_to_base(ds_base, ds_culled, workers=-1): + """ + Create maps from cells, edges and vertices on a culled mesh to the same + elements on a base mesh. All elements in the culled mesh must be in the + base mesh. + + Parameters + ---------- + ds_base : xarray.Dataset + The horizontal MPAS mesh before culling + + ds_culled : xarray.Dataset + The culled horizonal MPAS mesh + + workers : int, optional + The number of threads to use to query basemesh elements. The default + is all available threads (``workers=-1``) + + Returns + ------- + ds_map_culled_to_base : xarray.Dataset + A dataset with ``mapCulledToBaseCell``, ``mapCulledToBaseEdge`` and + ``mapCulledToBaseVertex``, each of which contains the base mesh index + corresponding to the same element from the culled mesh. + """ + ds_map_culled_to_base = xr.Dataset() + for dim, suffix in [('nCells', 'Cell'), + ('nEdges', 'Edge'), + ('nVertices', 'Vertex')]: + _map_culled_to_base_grid_type(ds_base, ds_culled, + ds_map_culled_to_base, dim, suffix, + workers) + + return ds_map_culled_to_base + + +def _map_culled_to_base_grid_type(ds_base, ds_culled, ds_map_culled_to_base, + dim, suffix, workers): + x_base = ds_base[f'x{suffix}'].values + y_base = ds_base[f'y{suffix}'].values + z_base = ds_base[f'z{suffix}'].values + + x_culled = ds_culled[f'x{suffix}'].values + y_culled = ds_culled[f'y{suffix}'].values + z_culled = ds_culled[f'z{suffix}'].values + + # create a map from lat-lon pairs to base-mesh cell indices + points = np.vstack((x_base, y_base, z_base)).T + + tree = KDTree(points) + + points = np.vstack((x_culled, y_culled, z_culled)).T + + _, culled_to_base_map = tree.query(points, workers=workers) + + ds_map_culled_to_base[f'mapCulledToBase{suffix}'] = \ + ((dim,), culled_to_base_map) From 43a42fd7775e0f61ace154e4546174a639504d33 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 13 Mar 2024 14:28:22 +0100 Subject: [PATCH 101/169] Add functions for culling an MPAS dataset --- conda_package/mpas_tools/mesh/cull.py | 125 +++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/conda_package/mpas_tools/mesh/cull.py b/conda_package/mpas_tools/mesh/cull.py index 0afdddbba..1d16fdc03 100644 --- a/conda_package/mpas_tools/mesh/cull.py +++ b/conda_package/mpas_tools/mesh/cull.py @@ -27,7 +27,7 @@ def write_map_culled_to_base(base_mesh_filename, culled_mesh_filename, corresponding to the same element from the culled mesh. workers : int, optional - The number of threads to use to query basemesh elements. The default + The number of threads to use to query base mesh elements. The default is all available threads (``workers=-1``) """ ds_base = xr.open_dataset(base_mesh_filename) @@ -52,7 +52,7 @@ def map_culled_to_base(ds_base, ds_culled, workers=-1): The culled horizonal MPAS mesh workers : int, optional - The number of threads to use to query basemesh elements. The default + The number of threads to use to query base mesh elements. The default is all available threads (``workers=-1``) Returns @@ -73,6 +73,127 @@ def map_culled_to_base(ds_base, ds_culled, workers=-1): return ds_map_culled_to_base +def write_culled_dataset(in_filename, out_filename, base_mesh_filename, + culled_mesh_filename, + map_culled_to_base_filename=None, workers=-1, + logger=None): + """ + Create a new dataset in which all fields from ``ds`` have been culled + from the base mesh to the culled mesh. Fields present in + ``ds_culled_mesh`` are copied over rather than culled from ``ds``. + + Parameters + ---------- + in_filename : str + A file containing an MPAS dataset to cull + + output_filename : str + A file to write the culled MPAS dataset to + + base_mesh_filename : str + A file with the horizontal MPAS mesh before culling + + culled_mesh_filename : str + A file with the culled horizonal MPAS mesh + + map_culled_to_base_filename : str, optional + A file with an existing map from the base to the culled mesh created + with ``write_map_culled_to_base()`` or ``map_culled_to_base()``. The + dataset will be created (but not returned or saved to disk) if it is + not passed as an argument. + + workers : int, optional + The number of threads to use to query base mesh elements. The default + is all available threads (``workers=-1``) + + logger : logging.Logger, optional + A logger for the output + """ + ds = xr.open_dataset(in_filename) + ds_base_mesh = xr.open_dataset(base_mesh_filename) + ds_culled_mesh = xr.open_dataset(culled_mesh_filename) + if map_culled_to_base_filename is None: + ds_map_culled_to_base = None + else: + ds_map_culled_to_base = xr.open_dataset(map_culled_to_base_filename) + + ds_culled = cull_dataset( + ds=ds, ds_base_mesh=ds_base_mesh, ds_culled_mesh=ds_culled_mesh, + ds_map_culled_to_base=ds_map_culled_to_base, + workers=workers, logger=logger) + write_netcdf(ds_culled, out_filename) + + +def cull_dataset(ds, ds_base_mesh, ds_culled_mesh, ds_map_culled_to_base=None, + workers=-1, logger=None): + """ + Create a new dataset in which all fields from ``ds`` have been culled + from the base mesh to the culled mesh. Fields present in + ``ds_culled_mesh`` are copied over rather than culled from ``ds``. + + Parameters + ---------- + ds : xarray.Dataset + An MPAS dataset to cull + + ds_base_mesh : xarray.Dataset + The horizontal MPAS mesh before culling + + ds_culled_mesh : xarray.Dataset + The culled horizonal MPAS mesh + + ds_map_culled_to_base : xarray.Dataset, optional + An existing map from the base to the culled mesh created with + ``write_map_culled_to_base()`` or ``map_culled_to_base()``. The dataset + will be created (but not returned or saved to disk) if it is not passed + as an argument. + + workers : int, optional + The number of threads to use to query base mesh elements. The default + is all available threads (``workers=-1``) + + logger : logging.Logger, optional + A logger for the output + + Returns + ------- + ds_culled : xarray.Dataset + An culled MPAS dataset + """ + if ds_map_culled_to_base is None: + if logger is not None: + logger.info('Creating culled-to-base mapping') + ds_map_culled_to_base = map_culled_to_base( + ds_base=ds_base_mesh, ds_culled=ds_culled_mesh, workers=workers) + + if logger is not None: + logger.info('Culling dataset') + ds_culled = ds + if 'nCells' in ds_culled.dims: + ds_culled = ds_culled.isel( + nCells=ds_map_culled_to_base['mapCulledToBaseCell'].values) + if 'nEdges' in ds_culled.dims: + ds_culled = ds_culled.isel( + nEdges=ds_map_culled_to_base['mapCulledToBaseEdge'].values) + if 'nVertices' in ds_culled.dims: + ds_culled = ds_culled.isel( + nVertices=ds_map_culled_to_base['mapCulledToBaseVertex'].values) + + if logger is not None: + logger.info('Replacing variables from culled mesh') + for var in ds.data_vars: + if var in ds_culled_mesh: + if logger is not None: + logger.info(f' replacing: {var}') + # replace this field with the version from the culled mesh + ds_culled[var] = ds_culled_mesh[var] + else: + if logger is not None: + logger.info(f' keeping: {var}') + + return ds_culled + + def _map_culled_to_base_grid_type(ds_base, ds_culled, ds_map_culled_to_base, dim, suffix, workers): x_base = ds_base[f'x{suffix}'].values From e147e4b0ddd85cd757acd4781e2d4ef958bd5983 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 13 Mar 2024 14:53:13 +0100 Subject: [PATCH 102/169] Update the docs --- conda_package/docs/api.rst | 10 ++++ conda_package/docs/mesh_conversion.rst | 73 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/conda_package/docs/api.rst b/conda_package/docs/api.rst index 0f6a06f88..95433fd0f 100644 --- a/conda_package/docs/api.rst +++ b/conda_package/docs/api.rst @@ -53,6 +53,16 @@ Mesh conversion cull mask +.. currentmodule:: mpas_tools.mesh.cull + +.. autosummary:: + :toctree: generated/ + + write_map_culled_to_base + map_culled_to_base + write_culled_dataset + cull_dataset + .. currentmodule:: mpas_tools.mesh.mask .. autosummary:: diff --git a/conda_package/docs/mesh_conversion.rst b/conda_package/docs/mesh_conversion.rst index 7affcd53d..2abf5f326 100644 --- a/conda_package/docs/mesh_conversion.rst +++ b/conda_package/docs/mesh_conversion.rst @@ -494,6 +494,79 @@ The command-line tool takes the following arguments: creation ('fork', 'spawn' or 'forkserver') +.. _cull_mpas_dataset: + +Culling MPAS Datasets +===================== + +The tools described in :ref:`cell_culler` can be used to create a culled +horizontal MPAS mesh. Once a culled MPAS mesh has been created, an MPAS +dataset on the unculled mesh can be cropped to the culled mesh using the +the :py:func:`mpas_tools.mesh.cull.cull_dataset()` or +:py:func:`mpas_tools.mesh.cull.write_culled_dataset()` functions. These +functions take a dataset (or filename) to crop as well as datasets (or +filenames) for the unculled and culled horizontal MPAS meshes. They return +(or write out) the culled version of the data set. Fields that exist in +the culled horizonal mesh are copied from the culled mesh, rather than cropped +from the dataset. This because we wish to keep the cropped horizontal mesh +exactly as it was produced by the culling tool, which may not correspond to +a cropped version of the field from the original mesh. For example, fields +are reindexed during culling and coordinates are recomputed. + +It may be useful to compute and store the maps from cells, edges and vertices +on the culled mesh back to the unculled mesh for reuse. This can be +accomplished by calling the :py:func:`mpas_tools.mesh.cull.map_culled_to_base()` +or :py:func:`mpas_tools.mesh.cull.write_map_culled_to_base()` functions. + +An example workflow that culls out ice-shelf cavities from an MPAS-Ocean +initial condition might look like the following. In this case the file +``culled_mesh.nc`` is a mesh where land (and the grounded portion of the +ice sheet) has been removed but where ice-shelf cavities are still present. +It serves as the "base" mesh for the purposes of this example. +``culled_mesh_no_isc.nc`` is created (if it doesn't already exist) with the +ice-shelf cavities removed as well, so it is the "culled" mesh in this example. +We store the mapping betwen the two horizontal meshes in +``no_isc_to_culled_map.nc`` in case we want to resue it later. The initial +condition is read from ``initial_state.nc`` and the culled version is written +to ``initial_state_no_isc.nc``: + +.. code-block:: python + + import os + + import xarray as xr + + from mpas_tools.io import write_netcdf + from mpas_tools.mesh.conversion import cull + from mpas_tools.mesh.cull import write_map_culled_to_base, write_culled_dataset + from mpas_tools.logging import LoggingContext + + + in_filename = 'initial_state.nc' + out_filename = 'initial_state_no_isc.nc' + base_mesh_filename = 'culled_mesh.nc' + culled_mesh_filename = 'culled_mesh_no_isc.nc' + map_filename = 'no_isc_to_culled_map.nc' + + if not os.path.exists(culled_mesh_filename): + ds_culled_mesh = xr.open_dataset(base_mesh_filename) + ds_init = xr.open_dataset(in_filename) + ds_culled_mesh['cullCell'] = ds_init.landIceMask + ds_culled_mesh_no_isc = cull(ds_culled_mesh) + write_netcdf(ds_culled_mesh_no_isc, culled_mesh_filename) + + if not os.path.exists(map_filename): + write_map_culled_to_base(base_mesh_filename=base_mesh_filename, + culled_mesh_filename=culled_mesh_filename, + out_filename=map_filename) + + with LoggingContext('test') as logger: + write_culled_dataset(in_filename=in_filename, out_filename=out_filename, + base_mesh_filename=base_mesh_filename, + culled_mesh_filename=culled_mesh_filename, + map_culled_to_base_filename=map_filename, + logger=logger) + .. _merge_split: Merging and Splitting From 73838e952100f245eeff0a15fbc0553f87909798 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 14 Mar 2024 18:18:17 +0100 Subject: [PATCH 103/169] Update to v0.32.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index bc24806e1..7ca72c038 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -44,6 +44,7 @@ Documentation On GitHub `v0.29.0`_ `0.29.0`_ `v0.30.0`_ `0.30.0`_ `v0.31.0`_ `0.31.0`_ +`v0.32.0`_ `0.32.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -122,3 +123,5 @@ Documentation On GitHub .. _`0.30.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.30.0 .. _`v0.31.0`: ../0.31.0/index.html .. _`0.31.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.31.0 +.. _`v0.32.0`: ../0.32.0/index.html +.. _`0.32.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.32.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 6b6c537c4..536166f7e 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 31, 0) +__version_info__ = (0, 32, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index bd09ec360..cf595d4b1 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.31.0" %} +{% set version = "0.32.0" %} package: name: {{ name|lower }} From 578aee7a23f84a68814aa3b93e642108c3a2167d Mon Sep 17 00:00:00 2001 From: hollyhan Date: Thu, 21 Mar 2024 17:49:19 -0600 Subject: [PATCH 104/169] Add south polar stereographic projection of a sphere --- .../set_lat_lon_fields_in_planar_grid.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mesh_tools/planar_grid_transformations/set_lat_lon_fields_in_planar_grid.py b/mesh_tools/planar_grid_transformations/set_lat_lon_fields_in_planar_grid.py index e4bfe13ee..c6d9065f7 100755 --- a/mesh_tools/planar_grid_transformations/set_lat_lon_fields_in_planar_grid.py +++ b/mesh_tools/planar_grid_transformations/set_lat_lon_fields_in_planar_grid.py @@ -31,6 +31,11 @@ # BEDMAP2 projection projections['ais-bedmap2'] = '+proj=stere +lat_ts=-71.0 +lat_0=-90 +lon_0=0.0 +k_0=1.0 +x_0=0.0 +y_0=0.0 +ellps=WGS84' # Note: BEDMAP2 elevations use EIGEN-GL04C geoid +# BEDMAP2 projection of sphere. This projection must be used to adjust MALI mesh when performing +# coupled MALI-SeaLevelModel simulations. Otherwise, ice mass won't be conserved between the MALI planar mesh and +# the spherical sea-level model grid during the post-processing (output analysis) step. +projections['ais-bedmap2-sphere'] = '+proj=stere +lat_ts=-71.0 +lat_0=-90 +lon_0=0.0 +k_0=1.0 +x_0=0.0 +y_0=0.0 +ellps=sphere' + # Standard Lat/Long projections['latlon'] = '+proj=longlat +ellps=WGS84' # =================================== From 8ca2471453b3a7f28d4ab9875e7e075370902f1f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 25 Mar 2024 14:41:05 -0500 Subject: [PATCH 105/169] Add a grow-mask argument to flood fill mask creation This allows a flood-fill to fill in a masked region rather than the entire (presumably culled) mesh. --- conda_package/mpas_tools/mesh/mask.py | 34 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/conda_package/mpas_tools/mesh/mask.py b/conda_package/mpas_tools/mesh/mask.py index 838a5a5d4..c14e20e37 100644 --- a/conda_package/mpas_tools/mesh/mask.py +++ b/conda_package/mpas_tools/mesh/mask.py @@ -387,7 +387,8 @@ def entry_point_compute_mpas_transect_masks(): engine=args.engine) -def compute_mpas_flood_fill_mask(dsMesh, fcSeed, logger=None, workers=-1): +def compute_mpas_flood_fill_mask(dsMesh, fcSeed, daGrow=None, logger=None, + workers=-1): """ Flood fill from the given set of seed points to create a contiguous mask. The flood fill operates using cellsOnCell, starting from the cells @@ -401,6 +402,11 @@ def compute_mpas_flood_fill_mask(dsMesh, fcSeed, logger=None, workers=-1): fcSeed : geometric_features.FeatureCollection A feature collection containing points at which to start the flood fill + daGrow : xarray.DataArray, optional + A data array of size ``nCells`` with a mask that is 1 anywhere the + flood fill is allowed to grow. The default is that the mask is all + ones. + logger : logging.Logger, optional A logger for the output if not stdout @@ -426,17 +432,22 @@ def compute_mpas_flood_fill_mask(dsMesh, fcSeed, logger=None, workers=-1): if logger is not None: logger.info(' Computing flood fill mask on cells:') - mask = _compute_seed_mask(fcSeed, lon, lat, workers) + seedMask = _compute_seed_mask(fcSeed, lon, lat, workers) cellsOnCell = dsMesh.cellsOnCell.values - 1 - mask = _flood_fill_mask(mask, cellsOnCell) + if daGrow is not None: + growMask = daGrow.values + else: + growMask = numpy.ones(dsMesh.sizes['nCells']) + + seedMask = _flood_fill_mask(seedMask, growMask, cellsOnCell) if logger is not None: logger.info(' Adding masks to dataset...') # create a new data array for the mask masksVarName = 'cellSeedMask' - dsMasks[masksVarName] = (('nCells',), numpy.array(mask, dtype=int)) + dsMasks[masksVarName] = (('nCells',), numpy.array(seedMask, dtype=int)) if logger is not None: logger.info(' Done.') @@ -1183,7 +1194,7 @@ def _compute_seed_mask(fcSeed, lon, lat, workers): return mask -def _flood_fill_mask(mask, cellsOnCell): +def _flood_fill_mask(seedMask, growMask, cellsOnCell): """ Flood fill starting with a mask of seed points """ @@ -1191,22 +1202,23 @@ def _flood_fill_mask(mask, cellsOnCell): maxNeighbors = cellsOnCell.shape[1] while True: - neighbors = cellsOnCell[mask == 1, :] + neighbors = cellsOnCell[seedMask == 1, :] maskCount = 0 for iNeighbor in range(maxNeighbors): indices = neighbors[:, iNeighbor] - # we only want to mask valid neighbors and locations that aren't - # already masked + # we only want to mask valid neighbors, locations that aren't + # already masked, and locations that we're allowed to flood indices = indices[indices >= 0] - localMask = mask[indices] == 0 + localMask = numpy.logical_and(seedMask[indices] == 0, + growMask[indices] == 1) maskCount += numpy.count_nonzero(localMask) indices = indices[localMask] - mask[indices] = 1 + seedMask[indices] = 1 if maskCount == 0: break - return mask + return seedMask def _compute_edge_sign(dsMesh, edgeMask, shape): From 3dc79517497a97c8da348f4f0c3232b2e7af22d3 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 25 Mar 2024 14:42:17 -0500 Subject: [PATCH 106/169] Update docs --- conda_package/docs/mesh_conversion.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/mesh_conversion.rst b/conda_package/docs/mesh_conversion.rst index 2abf5f326..414495be9 100644 --- a/conda_package/docs/mesh_conversion.rst +++ b/conda_package/docs/mesh_conversion.rst @@ -413,10 +413,14 @@ Computing a Flood-fill Mask The function :py:func:`mpas_tools.mesh.mask.compute_mpas_flood_fill_mask()` and the command-line tool ``compute_mpas_flood_fill_mask`` -fill in a mask, starting with the ocean points closest to the seed points +fill in a mask, starting with the cell centers closest to the seed points given in :py:class:`geometric_features.FeatureCollection` ``fcSeed``. This algorithm runs in serial, and will be more efficient the more seed points -are provided and the more widely scattered over the ocean they are. +are provided and the more widely scattered over the mesh they are. + +An optional ``daGrow`` argument to the function (not currently available from +the command-line tool) provides a mask into which the flood fill is allowed to +grow. The default is all ones. The resulting dataset contains a single variable: From ab0df5c326f80cbf4f38b085130ecd62a6120780 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 26 Mar 2024 06:16:08 -0500 Subject: [PATCH 107/169] Update to v0.33.0 --- conda_package/docs/versions.rst | 3 +++ conda_package/mpas_tools/__init__.py | 2 +- conda_package/recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conda_package/docs/versions.rst b/conda_package/docs/versions.rst index 7ca72c038..3aa7cc465 100644 --- a/conda_package/docs/versions.rst +++ b/conda_package/docs/versions.rst @@ -45,6 +45,7 @@ Documentation On GitHub `v0.30.0`_ `0.30.0`_ `v0.31.0`_ `0.31.0`_ `v0.32.0`_ `0.32.0`_ +`v0.33.0`_ `0.33.0`_ ================ =============== .. _`stable`: ../stable/index.html @@ -125,3 +126,5 @@ Documentation On GitHub .. _`0.31.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.31.0 .. _`v0.32.0`: ../0.32.0/index.html .. _`0.32.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.32.0 +.. _`v0.33.0`: ../0.33.0/index.html +.. _`0.33.0`: https://github.com/MPAS-Dev/MPAS-Tools/tree/0.33.0 diff --git a/conda_package/mpas_tools/__init__.py b/conda_package/mpas_tools/__init__.py index 536166f7e..221d50eb2 100644 --- a/conda_package/mpas_tools/__init__.py +++ b/conda_package/mpas_tools/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 32, 0) +__version_info__ = (0, 33, 0) __version__ = '.'.join(str(vi) for vi in __version_info__) diff --git a/conda_package/recipe/meta.yaml b/conda_package/recipe/meta.yaml index cf595d4b1..475536d1a 100644 --- a/conda_package/recipe/meta.yaml +++ b/conda_package/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "mpas_tools" %} -{% set version = "0.32.0" %} +{% set version = "0.33.0" %} package: name: {{ name|lower }} From 6b39f36581472d5ac4405c34f61fe1b67bba6870 Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Fri, 5 Apr 2024 13:36:11 -0600 Subject: [PATCH 108/169] add floodFillThawedIce.py Creates a new script, floodFillThawedIce.py, that deactivates frozen regions of the hydro domain by using a flood fill of thawed basal ice originating at the grounding line. Thickness of frozen ice is set to zero and a no-flow waterFluxMask is defined surrounding the frozen area. --- landice/mesh_tools_li/floodFillThawedIce.py | 97 +++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 landice/mesh_tools_li/floodFillThawedIce.py diff --git a/landice/mesh_tools_li/floodFillThawedIce.py b/landice/mesh_tools_li/floodFillThawedIce.py new file mode 100644 index 000000000..f34867d53 --- /dev/null +++ b/landice/mesh_tools_li/floodFillThawedIce.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +import mpas_tools +import numpy as np +import xarray as xr +from compass.landice.mesh import mpas_flood_fill +from optparse import OptionParser +import subprocess + +print("** Gathering information ...") +parser = OptionParser() +parser.add_option("-f", "--file", dest="file", metavar="FILE") +options, args = parser.parse_args() + +f = xr.open_dataset(options.file, decode_times=False, decode_cf=False) +groundedBasalMassBal = f['groundedBasalMassBal'][0,:] +cellsOnCell= f['cellsOnCell'][:].data +nEdgesOnCell = f['nEdgesOnCell'][:].data +thickness = f['thickness'][0,:].data +bedTopography = f['bedTopography'][0,:].data +edgesOnCell = f['edgesOnCell'][:,:].data +yEdge = f['yEdge'][:].data +dims = f.dims +nVertInterfaces = dims['nVertInterfaces'] +uReconstructX = f['uReconstructX'][0,:,nVertInterfaces].data +uReconstructY = f['uReconstructY'][0,:,nVertInterfaces].data + +groundedMask = ((thickness*910/1028+bedTopography)>0.0)*(thickness>0.0) +floatingMask = ((thickness*910/1028+bedTopography)<0.0)*(thickness>0.0) +oceanMask = (thickness==0.0)*(bedTopography<0.0) +landMask = (thickness==0.0)*(bedTopography>0.0) + +groundedMask = groundedMask.reshape((len(groundedMask),1)) +floatingMask = floatingMask.reshape(len(floatingMask),1) +oceanMask = oceanMask.reshape(len(oceanMask),1) +landMask = landMask.reshape(len(landMask),1) + +seedMask = np.zeros((len(nEdgesOnCell),1), 'float64') + +print("**Defining Seed and Grow Masks ...") +ind = np.where(groundedMask==1)[0] +for i in ind: + #identify grounded cells just inland of grounding line + for ii in range(nEdgesOnCell[i]): + # if ((groundedMask[cellsOnCell[i,ii]-1] == 0): + if ((floatingMask[cellsOnCell[i,ii]-1] == 1) | (oceanMask[cellsOnCell[i,ii]-1] == 1)): + seedMask[i] = 1 + +#identify grounded, thawed ice +basalSlidingSpeed = np.sqrt(uReconstructX**2 + uReconstructY**2) * 3.15e7 #convert to m/yr +UbThresh = 25 #m/yr – change frozen ice to thawed ice if basalSlidingSpeed is above UbThresh + +growMask = (abs(groundedBasalMassBal) > 0.0)*(basalSlidingSpeed > UbThresh) +growMask = growMask.expand_dims(dim='Time').T + +print("**Flood Filling ...") +keepMask = mpas_flood_fill(seedMask, growMask, cellsOnCell, nEdgesOnCell) + +print("Defining waterFluxMask ...") + +#zero out ice thickness where frozen ice or no thawed ice in contact with grounding line +thickness[keepMask!=1] = 0 +thickness = thickness.reshape(1,len(thickness)) + +#create zero flux mask on edges of unactive domain +waterFluxMask = np.zeros((len(yEdge),1),'int32') +for i in range(len(yEdge)): + cell1 = cellsOnEdge[i,0]-1 + cell2 = cellsOnEdge[i,1]-1 + if ((keepMask[cell1] == 1 and keepMask[cell2] == 0 and groundMask[cell1] == 1 and groundMask[cell2] == 1) \ + or (keepMask[cell2] == 1 and keepMask[cell1] == 0 and groundMask[cell1] and groundMask[cell2])): + waterFluxMask[i] = 2 + +print("Saving ....") + +f = f.drop_vars(['thickness']) +f = f.drop_vars(['xtime']) +f = f.drop_vars(['simulationStartTime']) + +basalMeltInput = abs(groundedBasalMassBal) +bmi = xr.DataArray(basalMeltInput.T.astype('int32'),dims=('Time','nEdges') +f['basalMeltInput'] = bmi + +wfm = xr.DataArray(waterFluxMask.T.astype('int32'),dims=('Time','nEdges')) +f['waterFluxMask'] = wfm + +thk = xr.DataArray(thickness.astype('float64'),dims=('Time','nCells')) +f['thickness'] = thk + +#Remove fill values automatically added by xarray +for varname in f.variables + if '_FillValue' in f.variables[varname].attrs + del f.variables.attrs['_FillValue'] + +f.to_netcdf("finalMaskedFile.nc") +f.close() + From ddfd3d05bc8f47745c9ccf3d17f0878b6689a828 Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Mon, 8 Apr 2024 10:49:44 -0600 Subject: [PATCH 109/169] UbThresh as input variable Establishes UbThresh in floodFillThawedIce.py as an input variable with a default value of 25 m/yr --- landice/mesh_tools_li/floodFillThawedIce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/floodFillThawedIce.py b/landice/mesh_tools_li/floodFillThawedIce.py index f34867d53..9f49154bf 100644 --- a/landice/mesh_tools_li/floodFillThawedIce.py +++ b/landice/mesh_tools_li/floodFillThawedIce.py @@ -10,6 +10,7 @@ print("** Gathering information ...") parser = OptionParser() parser.add_option("-f", "--file", dest="file", metavar="FILE") +parser.add_option("-u", "--UbThresh", dest="UbThresh", type="float", default=25, help="basal sliding threshold used to redefined frozen ice as thawed where sliding speed is above UbThresh (in m/yr)") options, args = parser.parse_args() f = xr.open_dataset(options.file, decode_times=False, decode_cf=False) @@ -48,7 +49,6 @@ #identify grounded, thawed ice basalSlidingSpeed = np.sqrt(uReconstructX**2 + uReconstructY**2) * 3.15e7 #convert to m/yr -UbThresh = 25 #m/yr – change frozen ice to thawed ice if basalSlidingSpeed is above UbThresh growMask = (abs(groundedBasalMassBal) > 0.0)*(basalSlidingSpeed > UbThresh) growMask = growMask.expand_dims(dim='Time').T From a9d4289f563d62a4ea3f8cfc822c0d1c385e859c Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Mon, 8 Apr 2024 16:25:54 -0600 Subject: [PATCH 110/169] PR cleanup 1 Makes minor edits to the PR in accordance with the first round of review that improve performance and clarity. --- landice/mesh_tools_li/floodFillThawedIce.py | 37 +++++++++++---------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/landice/mesh_tools_li/floodFillThawedIce.py b/landice/mesh_tools_li/floodFillThawedIce.py index 9f49154bf..70c9fb519 100644 --- a/landice/mesh_tools_li/floodFillThawedIce.py +++ b/landice/mesh_tools_li/floodFillThawedIce.py @@ -1,5 +1,10 @@ #!/usr/bin/env python - +''' +Uses output from MALI's thermal solver to determine where regions of the bed are frozen or thawed and masks of frozen cells from the hydro domain. +Only thawed cells in hydraulic contact with the grounding line are incorporated into the hydro domain. All other cells are set to zero ice thickness +and are surrounded by no-flow conditions with the waterFluxMask. An additional argument, "-u", will count any frozen ice as thawed if basal sliding speeds +exceed a threshold value (default is 25 m/yr). +''' import mpas_tools import numpy as np import xarray as xr @@ -10,26 +15,25 @@ print("** Gathering information ...") parser = OptionParser() parser.add_option("-f", "--file", dest="file", metavar="FILE") -parser.add_option("-u", "--UbThresh", dest="UbThresh", type="float", default=25, help="basal sliding threshold used to redefined frozen ice as thawed where sliding speed is above UbThresh (in m/yr)") +parser.add_option("-u", "--UbThresh", dest="UbThresh", type="float", default=25, help="basal sliding threshold used to redefine frozen ice as thawed where sliding speed is above UbThresh (in m/yr)") options, args = parser.parse_args() f = xr.open_dataset(options.file, decode_times=False, decode_cf=False) -groundedBasalMassBal = f['groundedBasalMassBal'][0,:] +groundedBasalMassBal = f['groundedBasalMassBal'][0,:].data cellsOnCell= f['cellsOnCell'][:].data nEdgesOnCell = f['nEdgesOnCell'][:].data thickness = f['thickness'][0,:].data bedTopography = f['bedTopography'][0,:].data edgesOnCell = f['edgesOnCell'][:,:].data yEdge = f['yEdge'][:].data -dims = f.dims -nVertInterfaces = dims['nVertInterfaces'] -uReconstructX = f['uReconstructX'][0,:,nVertInterfaces].data -uReconstructY = f['uReconstructY'][0,:,nVertInterfaces].data +nVertInterfaces = f.dims['nVertInterfaces'] +uReconstructX = f['uReconstructX'][0,:,nVertInterfaces-1].data +uReconstructY = f['uReconstructY'][0,:,nVertInterfaces-1].data groundedMask = ((thickness*910/1028+bedTopography)>0.0)*(thickness>0.0) -floatingMask = ((thickness*910/1028+bedTopography)<0.0)*(thickness>0.0) +floatingMask = ((thickness*910/1028+bedTopography)<=0.0)*(thickness>0.0) oceanMask = (thickness==0.0)*(bedTopography<0.0) -landMask = (thickness==0.0)*(bedTopography>0.0) +landMask = (thickness==0.0)*(bedTopography>=0.0) groundedMask = groundedMask.reshape((len(groundedMask),1)) floatingMask = floatingMask.reshape(len(floatingMask),1) @@ -43,14 +47,13 @@ for i in ind: #identify grounded cells just inland of grounding line for ii in range(nEdgesOnCell[i]): - # if ((groundedMask[cellsOnCell[i,ii]-1] == 0): if ((floatingMask[cellsOnCell[i,ii]-1] == 1) | (oceanMask[cellsOnCell[i,ii]-1] == 1)): seedMask[i] = 1 #identify grounded, thawed ice basalSlidingSpeed = np.sqrt(uReconstructX**2 + uReconstructY**2) * 3.15e7 #convert to m/yr -growMask = (abs(groundedBasalMassBal) > 0.0)*(basalSlidingSpeed > UbThresh) +growMask = np.logical_or(groundedBasalMassBal < 0.0, basalSlidingSpeed > options.UbThresh) growMask = growMask.expand_dims(dim='Time').T print("**Flood Filling ...") @@ -62,13 +65,13 @@ thickness[keepMask!=1] = 0 thickness = thickness.reshape(1,len(thickness)) -#create zero flux mask on edges of unactive domain +#create zero flux mask on edges of inactive domain waterFluxMask = np.zeros((len(yEdge),1),'int32') for i in range(len(yEdge)): cell1 = cellsOnEdge[i,0]-1 cell2 = cellsOnEdge[i,1]-1 if ((keepMask[cell1] == 1 and keepMask[cell2] == 0 and groundMask[cell1] == 1 and groundMask[cell2] == 1) \ - or (keepMask[cell2] == 1 and keepMask[cell1] == 0 and groundMask[cell1] and groundMask[cell2])): + or (keepMask[cell2] == 1 and keepMask[cell1] == 0 and groundMask[cell1] == 1 and groundMask[cell2] == 1)): waterFluxMask[i] = 2 print("Saving ....") @@ -77,8 +80,8 @@ f = f.drop_vars(['xtime']) f = f.drop_vars(['simulationStartTime']) -basalMeltInput = abs(groundedBasalMassBal) -bmi = xr.DataArray(basalMeltInput.T.astype('int32'),dims=('Time','nEdges') +basalMeltInput = -1.0 * np.minimum(0.0, groundedBasalMassBal) +bmi = xr.DataArray(basalMeltInput.T.astype('int32'),dims=('Time','nCells')) f['basalMeltInput'] = bmi wfm = xr.DataArray(waterFluxMask.T.astype('int32'),dims=('Time','nEdges')) @@ -88,8 +91,8 @@ f['thickness'] = thk #Remove fill values automatically added by xarray -for varname in f.variables - if '_FillValue' in f.variables[varname].attrs +for varname in f.variables: + if '_FillValue' in f.variables[varname].attrs: del f.variables.attrs['_FillValue'] f.to_netcdf("finalMaskedFile.nc") From 16e3850b27c331b9dc2646a150c5aec46fe89d3a Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Wed, 10 Apr 2024 14:34:42 -0600 Subject: [PATCH 111/169] script name change --- .../{floodFillThawedIce.py => flood_fill_thawed_ice.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename landice/mesh_tools_li/{floodFillThawedIce.py => flood_fill_thawed_ice.py} (100%) diff --git a/landice/mesh_tools_li/floodFillThawedIce.py b/landice/mesh_tools_li/flood_fill_thawed_ice.py similarity index 100% rename from landice/mesh_tools_li/floodFillThawedIce.py rename to landice/mesh_tools_li/flood_fill_thawed_ice.py From 3cafae60642e75ad3f95cec3ae113fc5e773e8ad Mon Sep 17 00:00:00 2001 From: alexolinhager <131483939+alexolinhager@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:33:09 -0700 Subject: [PATCH 112/169] Update landice/mesh_tools_li/floodFillThawedIce.py Fix typo in description Co-authored-by: Matt Hoffman --- landice/mesh_tools_li/flood_fill_thawed_ice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landice/mesh_tools_li/flood_fill_thawed_ice.py b/landice/mesh_tools_li/flood_fill_thawed_ice.py index 70c9fb519..392b773fd 100644 --- a/landice/mesh_tools_li/flood_fill_thawed_ice.py +++ b/landice/mesh_tools_li/flood_fill_thawed_ice.py @@ -1,6 +1,6 @@ #!/usr/bin/env python ''' -Uses output from MALI's thermal solver to determine where regions of the bed are frozen or thawed and masks of frozen cells from the hydro domain. +Uses output from MALI's thermal solver to determine where regions of the bed are frozen or thawed and masks off frozen cells from the hydro domain. Only thawed cells in hydraulic contact with the grounding line are incorporated into the hydro domain. All other cells are set to zero ice thickness and are surrounded by no-flow conditions with the waterFluxMask. An additional argument, "-u", will count any frozen ice as thawed if basal sliding speeds exceed a threshold value (default is 25 m/yr). From baecc1460974e1b71f569468d578eac25668b24e Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Wed, 10 Apr 2024 14:45:12 -0600 Subject: [PATCH 113/169] PR debugging and cleanup Fixes a few minor bugs in the script --- .../mesh_tools_li/flood_fill_thawed_ice.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/landice/mesh_tools_li/flood_fill_thawed_ice.py b/landice/mesh_tools_li/flood_fill_thawed_ice.py index 392b773fd..56d829d37 100644 --- a/landice/mesh_tools_li/flood_fill_thawed_ice.py +++ b/landice/mesh_tools_li/flood_fill_thawed_ice.py @@ -20,13 +20,14 @@ f = xr.open_dataset(options.file, decode_times=False, decode_cf=False) groundedBasalMassBal = f['groundedBasalMassBal'][0,:].data -cellsOnCell= f['cellsOnCell'][:].data +cellsOnCell= f['cellsOnCell'][:,:].data nEdgesOnCell = f['nEdgesOnCell'][:].data thickness = f['thickness'][0,:].data bedTopography = f['bedTopography'][0,:].data edgesOnCell = f['edgesOnCell'][:,:].data +cellsOnEdge = f['cellsOnEdge'][:,:].data yEdge = f['yEdge'][:].data -nVertInterfaces = f.dims['nVertInterfaces'] +nVertInterfaces = f.sizes['nVertInterfaces'] uReconstructX = f['uReconstructX'][0,:,nVertInterfaces-1].data uReconstructY = f['uReconstructY'][0,:,nVertInterfaces-1].data @@ -54,7 +55,7 @@ basalSlidingSpeed = np.sqrt(uReconstructX**2 + uReconstructY**2) * 3.15e7 #convert to m/yr growMask = np.logical_or(groundedBasalMassBal < 0.0, basalSlidingSpeed > options.UbThresh) -growMask = growMask.expand_dims(dim='Time').T +growMask = growMask.reshape(len(growMask),1) print("**Flood Filling ...") keepMask = mpas_flood_fill(seedMask, growMask, cellsOnCell, nEdgesOnCell) @@ -62,39 +63,46 @@ print("Defining waterFluxMask ...") #zero out ice thickness where frozen ice or no thawed ice in contact with grounding line +thickness = thickness.reshape(len(thickness),1) thickness[keepMask!=1] = 0 -thickness = thickness.reshape(1,len(thickness)) #create zero flux mask on edges of inactive domain waterFluxMask = np.zeros((len(yEdge),1),'int32') for i in range(len(yEdge)): cell1 = cellsOnEdge[i,0]-1 cell2 = cellsOnEdge[i,1]-1 - if ((keepMask[cell1] == 1 and keepMask[cell2] == 0 and groundMask[cell1] == 1 and groundMask[cell2] == 1) \ - or (keepMask[cell2] == 1 and keepMask[cell1] == 0 and groundMask[cell1] == 1 and groundMask[cell2] == 1)): + if ((keepMask[cell1] == 1 and keepMask[cell2] == 0 and groundedMask[cell1] == 1 and groundedMask[cell2] == 1) \ + or (keepMask[cell2] == 1 and keepMask[cell1] == 0 and groundedMask[cell1] == 1 and groundedMask[cell2] == 1)): waterFluxMask[i] = 2 print("Saving ....") +try: + f = f.drop_vars(['xtime']) +finally: + print("No xtime variable to delete") -f = f.drop_vars(['thickness']) -f = f.drop_vars(['xtime']) -f = f.drop_vars(['simulationStartTime']) +try: + f = f.drop_vars(['simulationStartTime']) +finally: + print("No simulationStartTime variable to delete") + +try: + f = f.drop_vars(['forcingTimeStamp']) +finally: + ("No forcingTimeStamp variable to delete") basalMeltInput = -1.0 * np.minimum(0.0, groundedBasalMassBal) -bmi = xr.DataArray(basalMeltInput.T.astype('int32'),dims=('Time','nCells')) +basalMeltInput = basalMeltInput.reshape(len(basalMeltInput), 1) +bmi = xr.DataArray(basalMeltInput.T.astype('float64'),dims=('Time','nCells')) f['basalMeltInput'] = bmi wfm = xr.DataArray(waterFluxMask.T.astype('int32'),dims=('Time','nEdges')) f['waterFluxMask'] = wfm -thk = xr.DataArray(thickness.astype('float64'),dims=('Time','nCells')) +thk = xr.DataArray(thickness.T.astype('float64'),dims=('Time','nCells')) f['thickness'] = thk -#Remove fill values automatically added by xarray -for varname in f.variables: - if '_FillValue' in f.variables[varname].attrs: - del f.variables.attrs['_FillValue'] - f.to_netcdf("finalMaskedFile.nc") f.close() +subprocess.run(["ncatted", "-a", "_FillValue,,d,,", "finalMaskedFile.nc"]) From 0ce3f01faf930442003e779e517cdd75ed0af942 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 12 Apr 2024 14:30:30 -0700 Subject: [PATCH 114/169] First pass at an ISMIP6 2500 forcing script. --- landice/mesh_tools_li/ISMIP6-2500_Forcing.py | 109 +++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 landice/mesh_tools_li/ISMIP6-2500_Forcing.py diff --git a/landice/mesh_tools_li/ISMIP6-2500_Forcing.py b/landice/mesh_tools_li/ISMIP6-2500_Forcing.py new file mode 100644 index 000000000..8027e699a --- /dev/null +++ b/landice/mesh_tools_li/ISMIP6-2500_Forcing.py @@ -0,0 +1,109 @@ +import os +import sys +import argparse +import numpy as np +import pandas as pd +import xarray as xr +from cftime import datetime +from mpas_tools.io import write_netcdf + +def __xtime2cftime(xtime_str, format="%Y-%m-%d_%H:%M:%S"): + + stripped_str = xtime_str.tobytes().decode("utf-8").strip().strip('\x00') + + return datetime.strptime(stripped_str, format) + +def parse_xtime(ds): + + times = list(map(__xtime2cftime, ds.xtime.values)) + + time_da = xr.DataArray(times, [('Time', times)]) + + return ds.assign_coords({"Time":time_da}) + +def generate_samples(ds, sampling_start, sampling_end, rng, repeat_period=200): + """ + """ + # + sampling_period = sampling_end - sampling_start + + # 0 index OK, or should it be min? Dim has to be sorted... + forcing_start = int(ds.Time.dt.year[0]) + + # get the offset for the indexes + idx_offset = sampling_start - forcing_start + + samples = idx_offset + rng.integers(0, sampling_period, repeat_period) + + return samples + +def extend_forcing(src_ds, sample_idxs): + + # create new time index based on hardcoded (for now) input start and + # end years with an calendar occurance on the first of every year + time_da = xr.cftime_range("2300-01-01", "2500-01-01", + freq="YS", inclusive="left", calendar="noleap") + # conver the CFtimeindex to a left justified string + xtime_da = time_da.strftime("%Y-%m-%d_%H:%M:%S").str.ljust(64) + + # create a "new" extended datatset with the xtime variable + new_ds = xr.Dataset({"xtime": ("Time", xtime_da)}) + # convert xtime from string to character array + new_ds["xtime"] = new_ds.xtime.astype("S") + + for var in src_ds: + # do not copy xtime, as it was created above + if var == "xtime": continue + + # only sample variables w/ Time dimension + if "Time" in src_ds[var].dims: + new_ds[var] = src_ds[var].isel(Time=sample_idxs).drop_vars("Time") + else: + new_ds[var] = src_ds[var].copy() + + return new_ds + + +def cli_parser(argv): + + parser = argparse.ArgumentParser(prog='ISMIP6 2500 Extensions') + + parser.add_argument('-i', '--input', type=str) + parser.add_argument('-o', '--output_filename', type=str) + parser.add_argument('-s', '--seed', type=int, default=4727) + parser.add_argument('--sampling_start', type=int, default=2270) + parser.add_argument('--sampling_end', type=int, default=2300) + parser.add_argument('--repeat_period', type=int, default=200) + + args, _ = parser.parse_known_args(argv) + + assert os.path.exists(args.input) + + path = os.path.dirname(args.input) + + output_filename = os.path.join(path, args.output_filename) + + return (args.input, output_filename, args.seed, + args.sampling_start, args.sampling_end, args.repeat_period) + + +if __name__ == "__main__": + + input_fp, output_fp, seed, start, end, period = cli_parser(sys.argv[1:]) + + ref_forcing = xr.open_dataset(input_fp) + ref_forcing = parse_xtime(ref_forcing) + + rng = np.random.default_rng(seed) + sample_idxs = generate_samples(ref_forcing, start, end, rng, period) + sample_years = ref_forcing.isel(Time=sample_idxs).Time.dt.year.values + + new_forcing = extend_forcing(ref_forcing, sample_idxs) + write_netcdf(new_forcing, output_fp) + + print("\n" + "*"*75) + print(f"ISMIP6 2500 forcing created by randomly sampling 2300 forcing " + f"files from {start}-{end} \n" + f"\nSampled file: {input_fp}" + f"\nExtened file: {output_fp}") + print("*"*75) From 44dea6ff52188c794c1c15931859d54abf7e8bca Mon Sep 17 00:00:00 2001 From: Alexander Hager Date: Wed, 17 Apr 2024 11:48:36 -0600 Subject: [PATCH 115/169] add adjust_thickness_at_false_ocean_cells.py Adds a script that adjusts the ice thickness at cells inland from the grounding line that are falsely identified as part of the ocean (based on ocean density floating criteria), in order to re-designate these cells as grounded. --- .../adjust_thickness_at_false_ocean_cells.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 landice/mesh_tools_li/adjust_thickness_at_false_ocean_cells.py diff --git a/landice/mesh_tools_li/adjust_thickness_at_false_ocean_cells.py b/landice/mesh_tools_li/adjust_thickness_at_false_ocean_cells.py new file mode 100644 index 000000000..75e4981c8 --- /dev/null +++ b/landice/mesh_tools_li/adjust_thickness_at_false_ocean_cells.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +''' +This script changes the designation of isolated cells inland from the grounding line that are wrongfully defined as +floating ice (and thus part of the ocean) when using the ocean density to define grounded/floating cells, as done in li_mask_is_grounded_ice. +Ocean and floating ice cells are flood filled from the edges of the domain, and any floating ice cells not in contact with the flood fill are +identified. Thickness of these cells is then manually altered to enforce the designation of grounded ice. +''' +import mpas_tools +import numpy as np +import xarray as xr +from compass.landice.mesh import mpas_flood_fill +from optparse import OptionParser +import subprocess + +print("** Gathering information ...") +parser = OptionParser() +parser.add_option("-f", "--file", dest="file", metavar="FILE") +options, args = parser.parse_args() + +f = xr.open_dataset(options.file, decode_times=False, decode_cf=False) +cellsOnCell= f['cellsOnCell'][:,:].data +nEdgesOnCell = f['nEdgesOnCell'][:].data +thickness = f['thickness'][0,:].data +bedTopography = f['bedTopography'][0,:].data +cellsOnEdge = f['cellsOnEdge'][:,:].data + +groundedIceMask = ((thickness*910/1028+bedTopography)>0.0)*(thickness>0.0) +floatingIceMask = ((thickness*910/1028+bedTopography)<=0.0)*(thickness>0.0) +oceanMask = (thickness==0.0)*(bedTopography<0.0) +landMask = (thickness==0.0)*(bedTopography>=0.0) + +seedMask = np.zeros((len(nEdgesOnCell),), 'float64') +growMask = floatingIceMask + oceanMask + +ind = np.where(oceanMask==1)[0] +for i in ind: + for ii in range(nEdgesOnCell[i]): + if (cellsOnCell[i,ii] == 0): + seedMask[i] = 1 + +print("**Flood Filling ...") + +keepMask = mpas_flood_fill(seedMask, growMask, cellsOnCell, nEdgesOnCell) + +ind = np.where(floatingIceMask==1)[0] +for i in ind: + if (keepMask[i] == 0): + thickness[i] = -bedTopography[i] * 1028/910 + 1e-10 #thickness necessary to achieve grounded ice (small margin past exact flotation) + +print("**Saving ...") + +seedMask = seedMask.reshape(1,len(seedMask)) +growMask = growMask.reshape(1,len(growMask)) +keepMask = keepMask.reshape(1,len(keepMask)) +thickness = thickness.reshape(1,len(thickness)) + +gm = xr.DataArray(growMask.astype('float64'),dims=('Time','nCells')) +f['growMask'] = gm + +sm = xr.DataArray(seedMask.astype('float64'),dims=('Time','nCells')) +f['seedMask'] = sm + +km = xr.DataArray(keepMask.astype('float64'),dims=('Time','nCells')) +f['keepMask'] = km + +thk = xr.DataArray(thickness.astype('float64'),dims=('Time','nCells')) +f['thickness'] = thk + +f.to_netcdf("modifiedThicknessDomain.nc") +f.close() + +subprocess.run(["ncatted", "-a", "_FillValue,,d,,", "modifiedThicknessDomain.nc"]) From 855d07c94a1d3efd46bbc46691a325e7b39b9ca5 Mon Sep 17 00:00:00 2001 From: Maciej Waruszewski Date: Sun, 28 Apr 2024 04:42:29 -0600 Subject: [PATCH 116/169] Fix planarIntersect formula in MPAS mesh converter --- mesh_tools/mesh_conversion_tools/pnt.h | 25 +++++++++++++------ .../mesh_conversion_tools_netcdf_c/pnt.h | 25 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/mesh_tools/mesh_conversion_tools/pnt.h b/mesh_tools/mesh_conversion_tools/pnt.h index 0c8100038..a07d733c3 100644 --- a/mesh_tools/mesh_conversion_tools/pnt.h +++ b/mesh_tools/mesh_conversion_tools/pnt.h @@ -376,18 +376,27 @@ pnt planarIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){ * planarIntersect is intended to compute the point of intersection * of the lines c1-c2 and v1-v2 in a plane. */ - double alpha_numerator, beta_numerator, denom; - double alpha, beta; + double cx_numerator, cy_numerator, denom; pnt c; - alpha_numerator = (v2.x - v1.x) * (c1.y - v2.y) - (v2.y - v1.y) * (c1.x - v2.x); -// beta_numerator = (c1.x - v2.x) * (c2.y - c1.y) - (c1.y - v2.y) * (c2.x - c1.x); - denom = (c2.x - c1.x) * (v2.y - v1.y) - (c2.y - c1.y) * (v2.x - v1.x); + double x1 = v1.x; + double y1 = v1.y; - alpha = alpha_numerator / denom; -// beta = beta_numerator / demon; + double x2 = v2.x; + double y2 = v2.y; - c = alpha * (v2 - v1) + v1; + double x3 = c1.x; + double y3 = c1.y; + + double x4 = c2.x; + double y4 = c2.y; + + cx_numerator = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4); + cy_numerator = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4); + denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + + c.x = cx_numerator / denom; + c.y = cy_numerator / denom; return c; }/*}}}*/ diff --git a/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h index cd587bb6b..ca2a35d42 100755 --- a/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h +++ b/mesh_tools/mesh_conversion_tools_netcdf_c/pnt.h @@ -376,18 +376,27 @@ pnt planarIntersect(const pnt &c1, const pnt &c2, const pnt &v1, const pnt &v2){ * planarIntersect is intended to compute the point of intersection * of the lines c1-c2 and v1-v2 in a plane. */ - double alpha_numerator, beta_numerator, denom; - double alpha, beta; + double cx_numerator, cy_numerator, denom; pnt c; - alpha_numerator = (v2.x - v1.x) * (c1.y - v2.y) - (v2.y - v1.y) * (c1.x - v2.x); -// beta_numerator = (c1.x - v2.x) * (c2.y - c1.y) - (c1.y - v2.y) * (c2.x - c1.x); - denom = (c2.x - c1.x) * (v2.y - v1.y) - (c2.y - c1.y) * (v2.x - v1.x); + double x1 = v1.x; + double y1 = v1.y; - alpha = alpha_numerator / denom; -// beta = beta_numerator / demon; + double x2 = v2.x; + double y2 = v2.y; - c = alpha * (v2 - v1) + v1; + double x3 = c1.x; + double y3 = c1.y; + + double x4 = c2.x; + double y4 = c2.y; + + cx_numerator = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4); + cy_numerator = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4); + denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + + c.x = cx_numerator / denom; + c.y = cy_numerator / denom; return c; }/*}}}*/ From 448a5c105d12b6edef50e73e5b12e78e3c30d4ba Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 29 Apr 2024 14:50:33 -0700 Subject: [PATCH 117/169] Clean up comments and add help strings to cli parser. --- landice/mesh_tools_li/ISMIP6-2500_Forcing.py | 48 ++++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/landice/mesh_tools_li/ISMIP6-2500_Forcing.py b/landice/mesh_tools_li/ISMIP6-2500_Forcing.py index 8027e699a..eb63d3230 100644 --- a/landice/mesh_tools_li/ISMIP6-2500_Forcing.py +++ b/landice/mesh_tools_li/ISMIP6-2500_Forcing.py @@ -8,12 +8,17 @@ from mpas_tools.io import write_netcdf def __xtime2cftime(xtime_str, format="%Y-%m-%d_%H:%M:%S"): - + """Convert a single xtime value to a cftime datetime object + """ + stripped_str = xtime_str.tobytes().decode("utf-8").strip().strip('\x00') return datetime.strptime(stripped_str, format) def parse_xtime(ds): + """Parse `xtime` vaules to cftime datetimes and create new coordinate + from parsed values, named `Time` + """ times = list(map(__xtime2cftime, ds.xtime.values)) @@ -22,12 +27,12 @@ def parse_xtime(ds): return ds.assign_coords({"Time":time_da}) def generate_samples(ds, sampling_start, sampling_end, rng, repeat_period=200): + """Generate array of sample indices from `Time` dimension of `ds` """ - """ - # + sampling_period = sampling_end - sampling_start - # 0 index OK, or should it be min? Dim has to be sorted... + # 0 index assumes `Time` coordinate is sorted forcing_start = int(ds.Time.dt.year[0]) # get the offset for the indexes @@ -38,7 +43,9 @@ def generate_samples(ds, sampling_start, sampling_end, rng, repeat_period=200): return samples def extend_forcing(src_ds, sample_idxs): - + """Create new "extended" dataset using the `sample_idxs` + """ + # create new time index based on hardcoded (for now) input start and # end years with an calendar occurance on the first of every year time_da = xr.cftime_range("2300-01-01", "2500-01-01", @@ -65,15 +72,22 @@ def extend_forcing(src_ds, sample_idxs): def cli_parser(argv): - + """Command line interfacr parser + """ parser = argparse.ArgumentParser(prog='ISMIP6 2500 Extensions') - parser.add_argument('-i', '--input', type=str) - parser.add_argument('-o', '--output_filename', type=str) - parser.add_argument('-s', '--seed', type=int, default=4727) - parser.add_argument('--sampling_start', type=int, default=2270) - parser.add_argument('--sampling_end', type=int, default=2300) - parser.add_argument('--repeat_period', type=int, default=200) + parser.add_argument('-i', '--input', type=str, + help="input forcing to sample from") + parser.add_argument('-o', '--output_filename', type=str, + help="output filename of extended forcing") + parser.add_argument('-s', '--seed', type=int, default=4727, + help="seed for random number generator") + parser.add_argument('--sampling_start', type=int, default=2270, + help="start of sampling window in reference dataset") + parser.add_argument('--sampling_end', type=int, default=2300, + help="end of sampling window in reference dataset") + parser.add_argument('--repeat_period', type=int, default=200, + help="length of extended forcing window") args, _ = parser.parse_known_args(argv) @@ -89,15 +103,21 @@ def cli_parser(argv): if __name__ == "__main__": + # parse the command line arguments input_fp, output_fp, seed, start, end, period = cli_parser(sys.argv[1:]) - + + # open the reference dataset and parse the xtime variable ref_forcing = xr.open_dataset(input_fp) ref_forcing = parse_xtime(ref_forcing) - + + # initialize the random number generator and create sample index array rng = np.random.default_rng(seed) sample_idxs = generate_samples(ref_forcing, start, end, rng, period) + + # find the sample years from the sample indices sample_years = ref_forcing.isel(Time=sample_idxs).Time.dt.year.values + # generate the extended forcing file and write it to disk new_forcing = extend_forcing(ref_forcing, sample_idxs) write_netcdf(new_forcing, output_fp) From 84a45c6723c4499e4c49bc4679a0f1b07eed466e Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Thu, 23 May 2024 19:36:15 -0700 Subject: [PATCH 118/169] Create a landice module for the mpas_tools conda package Create a landice module for the mpas_tools conda package, and add a visualization submodule containing plot_transect(). --- .../mpas_tools/landice/visualization.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 conda_package/mpas_tools/landice/visualization.py diff --git a/conda_package/mpas_tools/landice/visualization.py b/conda_package/mpas_tools/landice/visualization.py new file mode 100644 index 000000000..e834e818c --- /dev/null +++ b/conda_package/mpas_tools/landice/visualization.py @@ -0,0 +1,178 @@ +import numpy as np +import csv +from netCDF4 import Dataset +from scipy.interpolate import LinearNDInterpolator +import matplotlib.pyplot as plt +from matplotlib.pyplot import cm + + +def plot_transect(data_path, variable, times=[0], ax=None, + coords_file=None, x=None, y=None, + time_cmap_name="plasma", + temperature_cmap_name='YlGnBu_r'): + """ + Plot transects of desired variable from MALI output. + + Parameters + ---------- + data_path : str + Path to .nc file containing MALI mesh and variables to plot + + variable : str + MALI variable to plot. Can also be "geometry", which will + calculate upper and lower surfaces from thickness and bed topography. + + times : list of ints, optional + Time indices at which to plot variable. + + ax : matplotlib.axes._axes.Axes + Axes on which to plot variable + + coords_file : str, optional + Path to file containing coordinates for transect + + x : list of floats, optional + x coordinates defining transect if not using coords_file + + y : list of floats, optional + y coordinates defining transect if not using coords_file + + time_cmap_name : str, optional + Name of matplotlib colormap for multiple time levels of variable + + temperature_cmap_nam : str, optional + Name of matplotlib colormap for temperature transect + """ + + dataset = Dataset(data_path) + xCell = dataset.variables["xCell"][:] + yCell = dataset.variables["yCell"][:] + nCells = dataset.dimensions['nCells'].size + areaCell = dataset.variables["areaCell"][:] + layerThicknessFractions = dataset.variables["layerThicknessFractions"][:] + nVertLevels = dataset.dimensions['nVertLevels'].size + thk = dataset.variables["thickness"][:] + bed = dataset.variables["bedTopography"][:] + # ensure that times is a list, otherwise indexing will cause errors + times = list(times) + if "daysSinceStart" in dataset.variables.keys(): + use_yrs = True + yrs = dataset.variables["daysSinceStart"][:] / 365. + times_list = [f'{yrs[i]:.1f}' for i in times] + else: + use_yrs = False + times_list = [str(i) for i in times] + + # Replace -1 time index with last forward index of time array. + # It is unclear why these need to be in separate if-statements, but they do. + if -1 in times: + times[times.index(-1)] = int(dataset.dimensions['Time'].size - 1) + if '-1' in times_list: + times_list[times_list.index('-1')] = str(dataset.dimensions['Time'].size - 1) + + # Use coordinates from CSV file or x,y options, but not both. + # CSV file takes precedent if both are present. + if coords_file is not None: + x = [] + y = [] + with open(options.coords_file, newline='') as csvfile: + reader = csv.reader(csvfile, delimiter=',') + + for row in reader: + x.append(float(row[0])) + y.append(float(row[1])) + if [options.x_coords, options.y_coords] != [None, None]: + print('-c and -x/-y options were both provided. Reading from ', + f'{options.coords_file} and ignoring -x and -y settings.') + x = np.asarray(x) + y = np.asarray(y) + + # increase sampling to match highest mesh resolution + total_distance, = np.cumsum( np.sqrt( np.diff(x)**2. + np.diff(y)**2. ) ) + n_samples = int(round(total_distance / np.min(dataset.variables["dcEdge"][:]))) + x_interp = np.interp(np.linspace(0, len(x)-1, n_samples), + np.linspace(0, len(x)-1, len(x)), x) + y_interp = np.interp(np.linspace(0, len(y)-1, n_samples), + np.linspace(0, len(y)-1, len(y)), y) + + d_distance = np.zeros(len(x_interp)) + for ii in np.arange(1, len(x_interp)): + d_distance[ii] = np.sqrt( (x_interp[ii] - x_interp[ii-1])**2 + + (y_interp[ii] - y_interp[ii-1])**2 ) + + distance = np.cumsum(d_distance) / 1000. # in km for plotting + + # Handle colormaps + time_cmap = plt.get_cmap(time_cmap_name) + time_colors = time_cmap(np.linspace(0,1,len(times))) + + if variable in ["temperature", "geometry"]: + # Assume constant bed + bed_interpolator = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, bed[0,:]) + bed_transect = bed_interpolator(np.vstack((x_interp, y_interp)).T) + ax.plot(distance, bed_transect, color='black') + for i, time in enumerate(times): + thk_interpolator = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, thk[time,:]) + thk_transect = thk_interpolator(np.vstack((x_interp, y_interp)).T) + lower_surf = np.maximum( -910. / 1028. * thk_transect, bed_transect) + lower_surf_nan = lower_surf.copy() # for plotting + lower_surf_nan[thk_transect==0.] = np.nan + upper_surf = lower_surf + thk_transect + upper_surf_nan = upper_surf.copy() # for plotting + upper_surf_nan[thk_transect==0.] = np.nan + ax.plot(distance, lower_surf_nan, color=time_colors[i]) + ax.plot(distance, upper_surf_nan, color=time_colors[i]) + + if variable == "temperature": + time = times[-1] + if len(times) > 1: + print("Cannot plot temperature at more than one time." + " Only plotting temperature for final time.") + temperature = dataset.variables["temperature"][:] + layer_thk = np.zeros((len(thk_transect), nVertLevels + 1)) + layer_midpoints = np.zeros((len(thk_transect), nVertLevels)) + layer_interfaces = np.zeros((len(thk_transect), nVertLevels + 1)) + layer_thk[:,0] = 0. + for ii in range(len(thk_transect)): + layer_thk[ii,1:] = np.cumsum(layerThicknessFractions * + thk_transect[ii]) + layer_midpoints[ii,:] = upper_surf[ii] - (layer_thk[ii,1:] + + layer_thk[ii,0:-1]) / 2. + layer_interfaces[ii,:] = upper_surf[ii] - layer_thk[ii,:] + + temp_transect = np.zeros((len(x_interp), nVertLevels)) + for lev in range(nVertLevels): + print(f'Interpolating temperature for level {lev} of {nVertLevels}. ', end="") + temp_interpolant = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, + temperature[time,:,lev]) + temp_transect[:, lev] = temp_interpolant( + np.vstack((x_interp, y_interp)).T) + temp_transect[temp_transect == 0.] = np.nan + temp_plot = ax.pcolormesh(np.tile(distance, (nVertLevels+1,1)).T, + layer_interfaces[:,:], temp_transect[1:,:], + cmap=temperature_cmap_name, + vmin=240., vmax=273.15) + temp_cbar = plt.colorbar(temp_plot) + temp_cbar.set_label('Temperature (K)') + temp_cbar.ax.tick_params(labelsize=12) + + else: + var = dataset.variables[variable][:] + for i, time in enumerate(times): + var_interpolator = LinearNDInterpolator( + np.vstack((xCell, yCell)).T, var[i,:]) + var_transect = var_interpolator(np.vstack((x_interp, y_interp)).T) + ax.plot(distance, var_transect, color=time_colors[i]) + if (len(times) > 1): + time_cbar = plt.colorbar(cm.ScalarMappable(cmap=time_cmap_name), ax=ax) + time_cbar.ax.tick_params(labelsize=12) + if use_yrs: + time_cbar.set_label('Year') + else: + time_cbar.set_label('time index') + time_cbar.set_ticks(times / np.max(times)) + time_cbar.set_ticklabels(times_list) + From 477610396a9cfb4edf0fe5f44bb3dc828ebdebc6 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Fri, 24 May 2024 06:16:04 -0700 Subject: [PATCH 119/169] Add __init__.py --- conda_package/mpas_tools/landice/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 conda_package/mpas_tools/landice/__init__.py diff --git a/conda_package/mpas_tools/landice/__init__.py b/conda_package/mpas_tools/landice/__init__.py new file mode 100644 index 000000000..e69de29bb From 79472c156be26cd4f8814e150964bd5e08b48a3c Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Fri, 24 May 2024 13:27:20 -0700 Subject: [PATCH 120/169] Add plot_map(), plot_grounding_line() and helper functions Add plot_map(), plot_grounding_line() and helper functions to mpas_tools.landice.visualization. --- .../mpas_tools/landice/visualization.py | 337 +++++++++++++++++- 1 file changed, 332 insertions(+), 5 deletions(-) diff --git a/conda_package/mpas_tools/landice/visualization.py b/conda_package/mpas_tools/landice/visualization.py index e834e818c..54bcbdc90 100644 --- a/conda_package/mpas_tools/landice/visualization.py +++ b/conda_package/mpas_tools/landice/visualization.py @@ -4,9 +4,13 @@ from scipy.interpolate import LinearNDInterpolator import matplotlib.pyplot as plt from matplotlib.pyplot import cm +import matplotlib.pyplot as plt +import matplotlib.tri as tri +from matplotlib.colorbar import Colorbar +from matplotlib.colors import Normalize, TwoSlopeNorm -def plot_transect(data_path, variable, times=[0], ax=None, +def plot_transect(data_path, variable, ax, times=[0], coords_file=None, x=None, y=None, time_cmap_name="plasma", temperature_cmap_name='YlGnBu_r'): @@ -22,12 +26,12 @@ def plot_transect(data_path, variable, times=[0], ax=None, MALI variable to plot. Can also be "geometry", which will calculate upper and lower surfaces from thickness and bed topography. - times : list of ints, optional - Time indices at which to plot variable. - ax : matplotlib.axes._axes.Axes Axes on which to plot variable + times : list of ints, optional + Time indices at which to plot variable. + coords_file : str, optional Path to file containing coordinates for transect @@ -38,7 +42,7 @@ def plot_transect(data_path, variable, times=[0], ax=None, y coordinates defining transect if not using coords_file time_cmap_name : str, optional - Name of matplotlib colormap for multiple time levels of variable + Name of matplotlib colormap for multiple time levels odataset.variable temperature_cmap_nam : str, optional Name of matplotlib colormap for temperature transect @@ -176,3 +180,326 @@ def plot_transect(data_path, variable, times=[0], ax=None, time_cbar.set_ticks(times / np.max(times)) time_cbar.set_ticklabels(times_list) + +def plot_map(data_path, variable, ax, time=0, cmap=None, + vmin=None, vmax=None, log_plot=False, mesh_file=None, + triangles=None, plot_grounding_line=False): + """ + Plot map of MALI output + + Parameters + ---------- + data_path : str + Path to .nc file containing variables to plot. May contain + MALI mesh fields, or you can use the mesh_file argument. + + variable : str + MALI variable to plot. Can also be "geometry", which will + calculate upper and lower surfaces from thickness and bed topography. + + ax : matplotlib.axes._axes.Axes + Axes on which to plot variable + + time : int, optional + Time index at which to plot variable. + + cmap : str, optional + Name of matplotlib colormap for multiple time levels of variable + + vmin : float, optional + Minimum value to use for plotting. If not specified, the 1st + percentile value will be used (not weighted by cell area). + + vmax : float, optional + Maximum value to use for plotting. If not specified, the 99th + percentile value will be used (not weighted by cell area). + + log_plot : boolean, optional + Whether to plot log10(variable) + + mesh_file : str, optional + Optional file used to specify mesh variables. If not provided, mesh + variables will be read from data_path + + triangles : matplotlib.tri._triangulation.Triangulation, optional + Triangles to use for plotting. If not specified, + they will be calculated. + + plot_grounding_line : boolean, optional + Whether to plot the grounding line along with variable. + + Returns + ------- + var_plot : matplotlib.collections.PolyCollection + Plot of variable + + cbar : matplotlib.colorbar.Colorbar + Colorbar object corresponded to var_plot + + gl_plot : matplotlib.tri._tricontour.TriContourSet or None + Contour object of the grounding line, if plot_grounding_line=True + """ + + sec_per_year = 60. * 60. * 24. * 365. + + dataset = Dataset(data_path) + dataset.set_auto_mask(False) + + if triangles is None: + if mesh_file is None: + mesh = Dataset(data_path) + else: + mesh = Dataset(mesh_file) + + triangles, tri_mask = _get_triangles(mesh) + mesh.close() + + if variable == 'observedSpeed': + var_to_plot = np.sqrt(dataset.variables['observedSurfaceVelocityX'][:]**2 + + dataset.variables['observedSurfaceVelocityY'][:]**2) + else: + var_to_plot = dataset.variables[variable][:] + + if len(np.shape(var_to_plot)) == 1: + var_to_plot = var_to_plot.reshape((1, np.shape(var_to_plot)[0])) + + if 'Speed' in variable: + units = 'm yr^{-1}' + var_to_plot *= sec_per_year + else: + try: + units = dataset.variables[variable].units + except AttributeError: + units='no-units' + + default_colors = {'thickness' : 'Blues', + 'surfaceSpeed' : 'plasma', + 'basalSpeed' : 'plasma', + 'bedTopography' : 'BrBG', + 'floatingBasalMassBalApplied' : 'cividis' + } + # List of diverging colormaps for use in plotting bedTopography. + # I don't see a way around hard-coding this. + div_color_maps = ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', + 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic'] + + if cmap is None: + if variable in default_colors.keys(): + cmap = default_colors[variable] + else: + cmap = "viridis" + + if log_plot: + var_to_plot = np.log10(var_to_plot) + # Get rid of +/- inf values that ruin vmin and vmax + # calculations below. + var_to_plot[np.isinf(var_to_plot)] = np.nan + colorbar_label_prefix = 'log10 ' + else: + colorbar_label_prefix = '' + + # Set lower and upper bounds for plotting + if vmin is None: + # 0.1 m/yr is a pretty good lower bound for speed + first_quant = np.nanquantile(var_to_plot[time, :], 0.01) + if 'Speed' in variable and log_plot: + vmin = max(first_quant, -1.) + else: + vmin = first_quant + if vmax is None: + vmax = np.nanquantile(var_to_plot[time, :], 0.99) + # Plot bedTopography on an asymmetric colorbar if appropriate + if ( (variable == 'bedTopography') and + (np.nanquantile(var_to_plot[time, :], 0.99) > 0.) and + (cmap in div_color_maps) ): + norm = TwoSlopeNorm(vmin=vmin, vmax=vmax, vcenter=0.) + else: + norm = Normalize(vmin=vmin, vmax=vmax) + + var_plot = ax.tripcolor( + triangles, var_to_plot[time, :], cmap=cmap, + shading='flat', norm=norm) + ax.set_aspect('equal') + + cbar = plt.colorbar(ax=ax, mappable=var_plot, + orientation='vertical', + label=f'{colorbar_label_prefix}{variable} (${units}$)') + if plot_grounding_line: + valid_masks, grounding_line_mask, _, _, _ = _calculate_masks(dataset) + if valid_masks: + gl_plot = ax.tricontour(triangles, grounding_line_mask[time, :], + levels=[0.9999], colors='white', + linestyles='solid') + else: + gl_plot = None + else: + gl_plot = None + + return var_plot, cbar, gl_plot + + +def plot_grounding_lines(data_paths, ax, times=[0], + cmap="plasma_r", mesh_file=None, + triangles=None): + """ + Plot MALI grounding line at arbitrary number of times. + + Parameters + ---------- + data_paths : str or list of str + Path(s) to MALI file. May contain MALI mesh fields, + or you can use the mesh_file argument. + + ax : matplotlib.axes._axes.Axes + Axes on which to plot variable + + time : list of ints, optional + Time indices at which to plot variable. + + cmap : str, optional + Name of matplotlib colormap for multiple time levels of variable + + mesh_file : str, optional + Optional file used to specify mesh variables. If not provided, mesh + variables will be read from data_path + + triangles : matplotlib.tri._triangulation.Triangulation, optional + Triangles to use for plotting. If not specified, + they will be calculated. + + Returns + ------- + gl_plots : list of matplotlib.tri._tricontour.TriContourSet + List of grounding line contour objects + """ + + # Ensure data_path is a list for flexibility in loops + if type(data_paths) != list: + data_paths = [data_paths] + + # If triangles are not specified, use first + # data file to define mesh, or use mesh_file + if triangles is None: + if mesh_file is None: + mesh = Dataset(data_paths[0]) + else: + mesh = Dataset(mesh_file) + + triangles, tri_mask = _get_triangles(mesh) + mesh.close() + + # Loop over all files and time levels to create + # lists of the grounding lines, and their associated years + plot_times = [] + grounding_line_masks = [] + gl_plots = [] + for file in data_paths: + f = Dataset(file) + f.set_auto_mask(False) + if 'daysSinceStart' in f.variables.keys(): + yr = f.variables['daysSinceStart'][times] / 365. + else: + yr = times + plot_times.append(yr) + valid_masks, grounding_line_mask, _, _, _ = _calculate_masks(mesh) + if valid_masks: + for time in times: + grounding_line_masks.append(grounding_line_mask[time, :]) + f.close() + + # Determine mapping between plot time and colormap. + # If just plotting one time, then use maximum value + # of the specified colormap. + n_times = len(times) * len(data_paths) + gl_cmap = plt.get_cmap(cmap) + if n_times > 1: + plot_times = np.squeeze(np.ravel(plot_times)) + plot_times_norm = ( (plot_times - np.min(plot_times)) / + np.max(plot_times - np.min(plot_times)) ) + else: + plot_times_norm = np.ones_like(plot_times) + + time_colors = gl_cmap(plot_times_norm) + + for ii, mask in enumerate(grounding_line_masks): + gl_plots.append(ax.tricontour(triangles, mask, + levels=[0.9999], linestyles='solid', + colors=time_colors[ii, None])) + + if len(plot_times) > 1: + time_cbar = plt.colorbar(cm.ScalarMappable(cmap=cmap), ax=ax, + location='bottom', label="Grounding line year") + time_cbar.ax.tick_params(labelsize=12) + time_cbar.set_ticks(plot_times_norm) + time_cbar.set_ticklabels(str(i) for i in plot_times) + + return gl_plots + + +def _dist(i1, i2, xCell, yCell): + + dist = ((xCell[i1]-xCell[i2])**2 + (yCell[i1]-yCell[i2])**2)**0.5 + return dist + + +def _calculate_masks(dataset): + + # Set bitmask values + initial_extent_value = 1 + dynamic_value = 2 + float_value = 4 + grounding_line_value = 256 + rhoi = 910. + rhosw = 1028. + + if 'cellMask' in dataset.variables.keys(): + valid_masks = True + cellMask = dataset.variables["cellMask"][:] + float_mask = (cellMask & float_value) // float_value + dynamic_mask = (cellMask & dynamic_value) // dynamic_value + grounding_line_mask = (cellMask & grounding_line_value) // grounding_line_value + initial_extent_mask = (cellMask & initial_extent_value) // initial_extent_value + elif ( 'cellMask' not in dataset.variables.keys() and + 'thickness' in dataset.variables.keys() and + 'bedTopography' in dataset.variables.keys() ): + print(f'cellMask is not present in output file {run};' + ' calculating masks from ice thickness') + valid_masks = True + grounded_mask = (dataset.variables['thickness'][:] > + (-rhosw / rhoi * + dataset.variables['bedTopography'][:])) + # This isn't technically correct, but works for plotting + grounding_line_mask = grounded_mask.copy() + initial_extent_mask = (dataset.variables['thickness'][:] > 0.) + else: + print('cellMask and thickness and/or bedTopography' + f' not present in output file {run};' + ' Skipping mask calculation.') + valid_masks = False + + return valid_masks, grounding_line_mask, float_mask, dynamic_mask, initial_extent_mask + + +def _get_triangles(mesh): + + xCell = mesh.variables["xCell"][:] + yCell = mesh.variables["yCell"][:] + dcEdge = mesh.variables["dcEdge"][:] + + triang = tri.Triangulation(xCell, yCell) + tri_mask = np.zeros(len(triang.triangles)) + + # Maximum distance in m of edges between points. + # Make twice dcEdge to be safe + max_dist = np.max(dcEdge) * 2.0 + for t in range(len(triang.triangles)): + thisTri = triang.triangles[t, :] + if _dist(thisTri[0], thisTri[1], xCell, yCell) > max_dist: + tri_mask[t] = True + if _dist(thisTri[1], thisTri[2], xCell, yCell) > max_dist: + tri_mask[t] = True + if _dist(thisTri[0], thisTri[2], xCell, yCell) > max_dist: + tri_mask[t] = True + triang.set_mask(tri_mask) + + return triang, tri_mask From 3a66c7aa3f79374feec74260fc73dc892a141a12 Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 28 May 2024 11:51:43 -0700 Subject: [PATCH 121/169] Add ability to plot map of pre-computed field. Add ability to plot map of pre-computed field, rather than providing the name of a MALI variable. For example, you can calculate thickness change between two time levels or output files and pass that as the variable argument to landice.plot_map(). --- .../mpas_tools/landice/visualization.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/conda_package/mpas_tools/landice/visualization.py b/conda_package/mpas_tools/landice/visualization.py index 54bcbdc90..b729bb7c4 100644 --- a/conda_package/mpas_tools/landice/visualization.py +++ b/conda_package/mpas_tools/landice/visualization.py @@ -183,9 +183,11 @@ def plot_transect(data_path, variable, ax, times=[0], def plot_map(data_path, variable, ax, time=0, cmap=None, vmin=None, vmax=None, log_plot=False, mesh_file=None, - triangles=None, plot_grounding_line=False): + triangles=None, plot_grounding_line=False, + variable_name=None): """ - Plot map of MALI output + Plot map of MALI output either by specifying a variable name or + a pre-computed field. Parameters ---------- @@ -193,9 +195,10 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, Path to .nc file containing variables to plot. May contain MALI mesh fields, or you can use the mesh_file argument. - variable : str - MALI variable to plot. Can also be "geometry", which will - calculate upper and lower surfaces from thickness and bed topography. + variable : str or numpy.ndarray + MALI variable to plot. If a string is specified, the variable with that + name will be read from the .nc file at data_path. If a numpy array is + given, that array will be plotted directly. ax : matplotlib.axes._axes.Axes Axes on which to plot variable @@ -228,6 +231,9 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, plot_grounding_line : boolean, optional Whether to plot the grounding line along with variable. + variable_name : str + Name to use for colorbar if specifying `variable` as a numpy array. + Returns ------- var_plot : matplotlib.collections.PolyCollection @@ -254,11 +260,15 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, triangles, tri_mask = _get_triangles(mesh) mesh.close() - if variable == 'observedSpeed': - var_to_plot = np.sqrt(dataset.variables['observedSurfaceVelocityX'][:]**2 + - dataset.variables['observedSurfaceVelocityY'][:]**2) + if type(variable) is str: + variable_name = variable + if variable == 'observedSpeed': + var_to_plot = np.sqrt(dataset.variables['observedSurfaceVelocityX'][:]**2 + + dataset.variables['observedSurfaceVelocityY'][:]**2) + else: + var_to_plot = dataset.variables[variable][:] else: - var_to_plot = dataset.variables[variable][:] + var_to_plot = variable if len(np.shape(var_to_plot)) == 1: var_to_plot = var_to_plot.reshape((1, np.shape(var_to_plot)[0])) @@ -268,9 +278,11 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, var_to_plot *= sec_per_year else: try: - units = dataset.variables[variable].units + units = f'({dataset.variables[variable].units})' except AttributeError: - units='no-units' + units = "{}" # This leaves out units on the colorbar + except TypeError: + units = "{}" default_colors = {'thickness' : 'Blues', 'surfaceSpeed' : 'plasma', @@ -284,7 +296,7 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic'] if cmap is None: - if variable in default_colors.keys(): + if variable_name in default_colors.keys(): cmap = default_colors[variable] else: cmap = "viridis" @@ -309,7 +321,7 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, if vmax is None: vmax = np.nanquantile(var_to_plot[time, :], 0.99) # Plot bedTopography on an asymmetric colorbar if appropriate - if ( (variable == 'bedTopography') and + if ( (variable_name == 'bedTopography') and (np.nanquantile(var_to_plot[time, :], 0.99) > 0.) and (cmap in div_color_maps) ): norm = TwoSlopeNorm(vmin=vmin, vmax=vmax, vcenter=0.) @@ -323,7 +335,7 @@ def plot_map(data_path, variable, ax, time=0, cmap=None, cbar = plt.colorbar(ax=ax, mappable=var_plot, orientation='vertical', - label=f'{colorbar_label_prefix}{variable} (${units}$)') + label=f'{colorbar_label_prefix}{variable_name} ${units}$') if plot_grounding_line: valid_masks, grounding_line_mask, _, _, _ = _calculate_masks(dataset) if valid_masks: From fe7e3a0c0cf9c0607c04eeae0dc35795bf694f11 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 30 May 2024 05:19:27 -0500 Subject: [PATCH 122/169] Fix data directory in simple_seaice_partitions tool --- conda_package/mpas_tools/seaice/partition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda_package/mpas_tools/seaice/partition.py b/conda_package/mpas_tools/seaice/partition.py index dc7f7b496..7f1d22e6d 100644 --- a/conda_package/mpas_tools/seaice/partition.py +++ b/conda_package/mpas_tools/seaice/partition.py @@ -378,8 +378,8 @@ def simple_partitions(): print("Regrid to desired mesh...") filenameOut = "icePresent_regrid.nc" - meshFilenameSrc = os.path.join(data_dir, 'seaice_QU60km_polar.nc') - filenameData = os.path.join(data_dir, 'icePresent_QU60km_polar.nc') + meshFilenameSrc = os.path.join(args.dataDir, 'seaice_QU60km_polar.nc') + filenameData = os.path.join(args.dataDir, 'icePresent_QU60km_polar.nc') regrid_to_other_mesh( meshFilenameSrc=meshFilenameSrc, From 677346f306b1172e7a584f4adbedc7b5d7dac3af Mon Sep 17 00:00:00 2001 From: Trevor Hillebrand Date: Tue, 18 Jun 2024 11:04:57 -0700 Subject: [PATCH 123/169] Add documentation for landice module --- conda_package/docs/api.rst | 13 ++ conda_package/docs/images/ais_map.png | Bin 0 -> 3757139 bytes .../images/ais_map_with_grounding_lines.png | Bin 0 -> 2528257 bytes .../docs/images/ais_thickness_change_map.png | Bin 0 -> 753176 bytes .../images/thwaites_temperature_transects.png | Bin 0 -> 556276 bytes .../docs/images/thwaites_transect.png | Bin 0 -> 224851 bytes conda_package/docs/index.rst | 6 + conda_package/docs/landice/visualization.rst | 121 ++++++++++++++++++ 8 files changed, 140 insertions(+) create mode 100644 conda_package/docs/images/ais_map.png create mode 100644 conda_package/docs/images/ais_map_with_grounding_lines.png create mode 100644 conda_package/docs/images/ais_thickness_change_map.png create mode 100644 conda_package/docs/images/thwaites_temperature_transects.png create mode 100644 conda_package/docs/images/thwaites_transect.png create mode 100644 conda_package/docs/landice/visualization.rst diff --git a/conda_package/docs/api.rst b/conda_package/docs/api.rst index 95433fd0f..a14f213d5 100644 --- a/conda_package/docs/api.rst +++ b/conda_package/docs/api.rst @@ -170,6 +170,19 @@ CIME constants constants +Landice Tools +============= + +.. currentmodule:: mpas_tools.landice + +.. autosummary:: + :toctree: generated/ + + visualization + visualization.plot_transect + visualization.plot_map + visualization.plot_grounding_lines + Ocean Tools =========== diff --git a/conda_package/docs/images/ais_map.png b/conda_package/docs/images/ais_map.png new file mode 100644 index 0000000000000000000000000000000000000000..e3539fb074142d873329c31310ec78a9e3b9bad5 GIT binary patch literal 3757139 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∋k z1c;Cp;5WnqQkR*XEef<}?=eE-F45$tWN-k#V4b6(d$z@6H>UU~*(!_n^pB#d=isiD zxw)s2YwF17cHpGHUN^!nk4Mtu?Hft&CL%X_ELgW7@k_n$_dreShv9`gTNI`ftlU38 z7PSRd38ljqP@58jZPHeDSRP0^7oR$Q+b@0A2=~0}VtZv6o1; ze+;0(Z>xwZqye-Fd&+`+9QYG>2T%^b=zt5=0{#nyK*F0xLzl$Q0cse!3goJ+mX;Q0 ziSXa%iXEj za=^Ikx}h{{&xr@|KlMdcp%xYDj%EH`^EdiS^EdcIfOqZDHPj{@CGDUS@OdA8cvv}` z1(h-&Y^+^fT^l?hkl40#T;ojjT9*Pvs+e?exPLSQsZ;ASL@z0QY$HVG;q(`o-4 zx{|Ll-;5RoVZmIR<_YqwG5O~y$-jc=lyCiM^K<#lz(}P51z6 z0Klor)Mq@{w?R7fS#i7K{L2FGJ~Qt&V;C?wc^pOudPluS1Nr#*HOs4L>x+&ofk^Wc z=;Qa>0}R)#B>%a$2|be6)&sn8u;N1Yx12bb3PR@$@O~!p#>pbHBoZz{j*x)eXu-A) z*r8ImB&*JD0$)d!I6nfUKyhyrsA>wN@-hmfLO0n=T7@kz>;Nuw93|Pzc4gW-CevMx zFM+!KaFBPp`RmZnef1Z|jwd^v`qrQd?OH4w4eIcRrshIl#vE&bsAT}aa~u@78+aH? z4;qel2MNL32W4bK~G3?gZ=I02&JV*Z&v%2qrA+SejzB$h=#czN^SyR3o<1%C?Qi2NQCrphz={6; zZ2)umlVQ2L4W!Se%K)Sxx->)=DOFP~@pkmpDFxOV9x)YbFr5}E)XYy<4gtDLIKgcR zI3X{1tH!p|A+5ujvn)O0Ummc$(htzpX#UtS|4rJ?Vv4&w)AybEm@DW9dSQC=I+?m|^l8BnBQqPc&8Rx~ghvTLYz zQJ^+$o11c_l3lB7$3?!5M82(+=$<7`Js$OsT%=a^lr~(9w!V|0$Rqfb6xR{}l#p8N zvs2EGItkn~U;n_H1*)MWlHOt`z}#eGH(jt>XNt9WVf)`NR4*2mHO=_X|K%q4CAyb= zb-7X{9dfp}hhB*}z8Ji=^kzHGn1<_lS9pe?r0Uub2_0)mM^9E7}|*Pztd`1q{9Jn;vON?oE+ zsGg2EqUUSImq7S75A>EpV)8LTf;(6ymP;%?%Ry@@=V!*}(u=G@f7Y|L4QYPfHTcf2 zXrmqt6gpl97I+0RqL`+Yz|c0DXBJb(^DMCJ`vvhLVZ4&sRuiqrDA%{u`ln(Y2~~7J zjOsMLBIcb3Mlsx-3E7cL62x=-UJ_R~-Oq+wSvD zgC#gmApC~&D8N!Ay<|YBW!DX4f$;e?Gx3APz-(6|v;d8mu(GnkN2ylvmLLAc9p#V2 zHg_M((YiD`@+$74Vj>s5#=KJji;YX%^NK6|t17MCczrup+95qXooQMIM4iz*WbN|2 zK@Iexd_F%G+b_hnWfoHq@_qwDn6YQyX0K|K7ll;O`q70e8T#TGY5G^aP0qpNKF z{^FC`a`y{gmz;7}i1Htvn645P*z=V#0P)fJbGVQ|KI=nCc6GuvSxn&r5>^GF;l;$TJ1%sw}4X|XRc2~;Q(-9E&K%n z*E-8*UWM6qk5US}GmTZ=CXK^Dvx*McEqoN<(ga{>35&y|0HPAT-Ma>Hp<1L#X_Vg1 zhXI=J46mu{`z@sHp-;ZeN{8yJBhF3f9!*(n%h7)$79VEsWZyhZC=|fl6K*0K?TKtB zb$k5wEsP>%AdEz`NR$bzsS2}Z)X9}tW@WLEKhV@lR&VHabYQ`;_%P6~AlC13L4^hB z$t&vW**%GgiSD3uEzyJ|ecp2wdCYe4QTnQvJUP2tAgN~RJ>n@nSFB>-BeLu3w4OLe z)bxEs`SXQCc^NM65^ zE}&MRuCzNK&68_*9bA(rD!D$4H@mo48sI-fpdsN8buGTMFM&7Mo*<=a4~$CsdLt|q z@T3dXK;TwQ2$DW!#+xhV*H|@1ngY{--fE{B12rGe^Jvg+t6H#;Ou9(|CJ~e|f;S{J zB-Yu$XoEowgAxXcJ&Ve^akBOB4U`8?g*N#D7htd6_ z=@e2$$?$y^8aigG%BMOiE1#ShcDT5_>|nn$JWoqT_>k}V1>ej>aR;#s!|MmLpH`la z%C5we5+mO?myq{%Ti**hb97DP!&l~XR=xfr7!7Pm-(Yz{w+GTZ2|ozUJusDt5P|v9 zZSvpC!$Sn!=oTO?ylG$N(5PyD&?bupiIyhF^8C`a z1yQvQS!v|yHu|(2_5~z~%Gr#0yAkp>;q?ok_3mC<4g8*|W5V>SXR4Mzgoj^eB|UJ! zByQ4Sn|7)zk98&?dkWGKd4`3*s;Q~bfm(bHsj@G&*MkQS`21Cg3b+@ZrxcaDzE!d5 zCW!ZU#j&p)Zqe|DpW|6Ngu7{$sedARyiJQ+Bf4{CJTRnQDUoZ|{&ZGm;nTQ~!ToDa zl@6BVeQ|L`fn&A!>D-DZlrhG#p%V*N)~icIaHB}}t4y(fRa@z5s4op+g@>o7V7Dr0 zznUOGLwg!Dx;IJ~z*B^#5THpHz}8K`K*X0`EJ6}WG6L{#iN_^XVN;^69G`!v8^%1_hk zsHS4`tkH1dQbDf(_LYQa{?hemap$B8%|J~xiHsUap2gSJK6#0EkJ_K&?b1JEZKQs# zMR6|+-I9v5wfa|=B9~{HnDh3KEB+P;=J?(|1vFM6%gelX9-&k^~FBJ${nG2 zLT~ivOWGR=sna4FdcN{u#=Gs|rzVLeRF^;KYD%XREbYZKoL(`6bl@a{lOCac#flt84H^3ylj{>S4M6UugHA4nirUdWJQMpX7?+#` zvIzOYpnC3>SnRdXJYjjmZ}}Yc)}nivJ}I*zJCYe5-rF@Y_1GdZt|fC}M)1?zlDkm< zZOt^fSS~)H+^qESry+MGfS+l~bRpFm9>l~J=VhOsTJ3(mnwB;w`IfgVwx}vM`5GqC zT4Y;Y&qly}&BK}MhnSrCs7pXARU&c0onMtie>}kBY2eyVTMi+S*-4US6*4&AcEoBj zQ90W@_cn+xUxe2}|1;3^umN*8DJdzlb92`W3=9tQ3zs8#-RM*CM5vty>V4GI7dgr3 zK||et>2{N9KhHCj3;)b~Ac(vS{d~a?P7>%9ZhnFUV8HT2;Z|r_84n&F{VI%>cQP(>qjYvO_Ic=xMEKXQ7BEop=$WRm+5!;` zdO>JWxwnX@xWp8DaYDi7vVynso8U%X%>Fs}llq)7dd98Ol`$eb`J=bd63&u#cYM|? znQ*u`kXDJQkI|xaBn@40k=QvxpU@*fyh^$Cg}h*v--c|7d+Mg+aZ zt4Jgg^vAkJMpAlm4YF^XT(IIzu=rOnJO#nvE2ZM2nseh+rlL7lTp2a5b*$EG*0e%d z^tc$IS~b=|R+IO*np(P1WnO8JYf0R&DsxR%BO2rlg=K7#BqgPrlv;C_{#m4H8;?_t zu#5f4?mm+Jos8R9@1gGJYfHTleAu7t3CdC8yGnA--#%KlsJMJ?HhrSUqwIj*nG;D2 zhk2T4_?dq}TxbJm*cH1^HPfAsX><_?sSwKpfAS}A_8YfWuLU?;rTk3V#r}10X}srt zMBjAI4cZf%6#R*J-fNlSu*Hpqb-Ez53?(_2>(R+R8_gSQ1)>*ykr=<%lHwQ z+6KOWbR{yI5Fz zXyW6HZbwMopB=c&J_MRBJ@ax z?RjY{10}LO(UVa|=83Q%tlm787zE?h3G6hTUAPd895^d0Qe9ORO2pLGzM260c(pjb^bh1N)KJX-tK z1g`b?7FeMJ>7ESwE|pBc3ML6odi&b_2cHZ`-Dt9ka>hNG3M)?cKBCbz*!5h*sKWhn zbVjDX(O?`}+r~pa_^twX{-Ti=!`H|-XAv{8q*O#F!Zf4BVXP{FvdNk7h&>sY`Gh?* zmp2xqbxn)pv>FNqjtaQ#rRr+8<2=CvR#TLH@R9}A=1BSkW_fkeg9_-qiZbjxWq-7Y zU|TRmQ*&)j4Ugc>uj%+N;sVuT9s7+mijbC6jaUa=@e{Gl)}N|MYxG}JO8gH6N;y^| zUPq^O8aw$oGAJOF%Mo~=3X^Pxa#m$RVWIIl*0i7}to|;6y%8zWB*+ud>$3N$h*O00 zw$ae8+N-jHZ5X)B{_GF*nBO+s?fq_GU0x?1NYTS(r|B!b0~H75B8W!QQ@9*RIZb^h zUibwbY<|A}kUn6368C^bi`fli7QT9OV@r}UO$T;s&okM|^jOHX<-lp$-908(=$p=K2N;>*Go{G%Q z-P7Kzy}@P=IL-1G*bZH=`COIZ-PhfA){oN;iS+xX1KvLU1_1&mxKsjCMYQ(evjLh+ z6_IuFdIptZM3VYkuF|L=lQnWb-AV`gh0wiQQ2>b_bP(P%CkDPQ$)#WZlO`SL6YwY!ug@H+TG$nrao7xrzjC8 zEj%tMU~j$zH{Vij^?9d=U>^W^?<%Uz`YLPOG{_4dgp3m21#H8c z9KH_{_M}LvE=`tz!qri#rQv`=9*e*TcVEkl?(J<@6QBLm;p`pRPd;&QA0I{BLBjgZ z`uL~|lS#X*sdf}lIZCxs(i6Ic#W2R=+uZ3L#@URSN*$`0EFe9=A6L?wf)8;)1YbHt_^N*TT}zflPkSV7>(V5%O<)i`>RJzw)I zGc2*F+C@J1-~qeq`z?BJwwfh1&hM+>REIz4pq9p!ly;wRC@Yn*!N&<|M5EarehD%f zeuMBF9Dw-4*LwD7UsuzKJ`m<`l8DV1f`|(r#1S z!El1gM-_Y5jOcnnEis+Q+o?BldOr-DRty_>TP)=SsJx}Nf{NTo<7+DZ z1bje9m)0eeu!S%Ag;Bt869vsH?Cl=H^s;{E(pW-np3=Q?1-@miH!f@8uz!a98=Z>TO09uS=KOxz&Jm)s5WYKPb%Jv70HiGV%1<@mc5GK%we!`+Q2$ z1)1~R4r<5rV8n^oS<-yrV>RT8?|aX$XlX0@mFKxW0^T$|b$puFyh;&J{TPubvr5Gg zQPndVy3S6x+6K;|H8-%koeyeC(55kL3M~uZV{%b|XGIk37=O@m>|l+VX3^w3M6^gx zvyY-da&+N%N=|rGR+^0)%MYAwcog>ujVGB6`VH!Hb`9(M!xk2XV$;Su z7X;p9lpn=!U5(Kjw3FtW1ny+7S|0blnEBI{`EY=uePgbP&B5SpDvbwH>eu%6whxr% z>hg>LV{8toddPpr1pkSDOi$fi^gMk4xF2sQZ@o<>KCZaC8N1e`+f)f92nwPpJ)Ncr zJBJQJ&WW8mJbE&r$7x`o?DSA6E!*Yo!O4_8-Ft!U_739}u~_+rGMHXJNxOWAwfsWD zRn=a~gHRLT*`N+bGbevq3(3kVIYHs~T=Vd-dFKs30UH;pnqOiNXEVUsVxk;00rHma z=_X`w*I#5ItTi|5M)%yM@t?mQ(gx@TWtJF2-Gd_K=!W0d;fez?h>Y$D$}-6?va<=Wekmm7a&NZzQ*AF}Wu@@NzK@zy zar(LV?9Dj-?5to=GNpU>KhfIff8NHUFYft?cFFTHcJGjSk=V&qk)~O@jk94oKBlFt zHBQR%j5S9f94X;ybf)L`lN~vo^Rpk|v3Z82 z^09N>(N9xVQEz628__F|Ebm$!>-_tTCCj6az9#saW8$|{fO6QDoN-Dfh>Lb z2&>kK)gGGlcxVh?e0dY$bvA^epTc5egA|KO8tc*Xd(Ga==U%D|9 z&aD-UNjfM2cy0}&Ph})UBTE$!6%6KFetv{esSKUu?`3V{t#{i?v}_cXaLWusBC9Fc z^1PCJzMD@Ft<8lJ#mo|0c>4&vDS{FMZ|#ZUn6?PLn2scV;5jEUhMnNz8P)j;CjM}_ zpM1$^jBVcCU%z!3^}fYJ(0d6$Wk2XS99@agRuQvcUUSl2FU(d^)#xf`tn%6zJ*J%uD85i{o6Xj8+GOYp;}C z&;EwHyr?%$7|G@ZS+93SOKU;ZSwk#rkRLyAGpXG?M}Ph(KBTL4`JRctQ_=FwvOdx* zEbDhFk7J6en6LXPrQc@`|3Nf%cW}5tu6<&HpQ`fhq?3dJLUKcc`;{vVx&sn7VSLJF zJ%-mu$9z;uYI4xgPL)DIpI1$x3u z!p5Jz*fZcQ!&?L#d4N8CKdZb*jB`?Cum>11@T|ZQ&*2=rs-#z6g@6+}UBRUW}IFuMuYHc^_ z{GfAWl`SZd30k_<(=CcmI0Y^ME1a_+EO=0Vn!#=>4xMVu?Xf4`dQ+*TA$d?YM?1Jm zbX%eSsRLX_<*%vAqIJx+Hg3v9e!kWK1};wOpX@bn8Av4-MSd=6TBwAfOS5v! zMX|gi`SpH}s`CqU4bufn-` zdT}Q1T@%HWY*<2Rsp*+4W`9gqigi<|7KBa(67w-4ppe=0G%w{DznE;~=~bJlOse|j zgPW#wJado{cfC4X_)%19W3+!bMf7XUqlj;b#&i@ZV_DSq*uHy$S8@ z#%BZFyd}m4L12aSM(z*G+#iM+BZ5L!*C41N<JPHE&J((TN?zFLU^vYhcN?Sbr*?m+ z1cc$r=^o>m>a>GnTG8Vx4t|4A>`&b)&T%>QNrmng72ab`4tZ?bTs+L1mYbRI{_!1# z^##_A7mLgyOS?>=i#njZ=qUbU+iY=gJ6Evo7swxeQJ9v3kCZKy$QgCDS`9gr{!mFu z)tJfWbk9h}X}4ca+Bu1l(^B0^p6L(W^;$}P_5((b-pL+Snj+Jyn>yTMF@(lYjd})) zr7BDFa1$ukYo#s)ix@u}gJ{87$K1eCV7JY|8mF3AiYACY*3dkraAhUW93x_7HDOek zm#a$OcrA+&TdHH>Q7(F#niU7Nstq9i2$*wIH}NJyZ=$4WR#Oqqsv-xQfxpRY+uZ1O z_Qrf=#bb}YHOyU~9=y1|F;kzpC-OFeI(T`Y1amMznf1JfGLdT_(X=aGrIPJg*!^_% zmFf}?NzKiy z&>~AbHO~y6658vc<8OjE7!lc9#((&5)2Dk9^pWA=r)%2SVe7Fne>e4L>w&Jz7(~+H zVtaCT(>7LRS1%#4H#L?|_;Bk?f$@4(PUi6lZWf*@eqgt~{Z+3$+S}F2NQ3?f=J^DM>s58Xd4hulpQdgE+=GNaI# z%P3wGV^xWEa(=zoqkpD*+c1dF#3y()awUJR$W%A=5Py>2lGQ$Zy|)GECFo)mF2Lav z9}`zf%Cxj!p}Vs?y}e4TO-OgHh}JlZi$;i9e{RV$)c4<#lytbrlem28Ej9sISWhae zs}Hv{y^6bgVuyOR{pb221=FTa{jH)h3ONjLRt%B(rr8+)kzWk1%+kil_t&aX1J?;_ zcf^ZBJ7QW7Ujk8^E{caG1$BOy@#jhv%?ZV|L^uIBuo{ZiKOOu0aF(;}&E%c0;`xUl z-(aQCcxT`$0v_OaGBYIZd2^e+d5H|rwQqV>o;OQSRCf3aUnAZW?GeqDD=D>e^f{|# zra4Dkc*g`r*eV{^GFIAVKU*-;k-oBo$|y`T#pcv!G~J*Vm|+)#&nUeat7E z-*XU1!2G_D28IWvA+o@cJ>F(-Rw0$I376X~CWjG$VUKMK#IS)u*k0hjbj?alp)qeM zXH#Gas2bqYg7(cor_up=7bpAHQCOwc&aZ^$1lX*XbcvjX3Vi^%Lk@H!@wAwgv zlde=QBrBL=HI-#-Sd`CPuMTO_0S|WA;6CFuYV%a6sg4z zZ;U_F+_8K$PQY_54~OjfG-JqMhJvtEUOeoi6%6Kawl-Gk*n*js3BN3JTK(b_C?$Q{ zT7gY(Z_yL2a_dcOo*^MNMJYQ=I{rzp<19MEG%@jQwSR}dRDk(rh1*?*x0qP=Q{A*1 z7Jd3>GSYqhey+~6bhyh%#RELGT&;+m`-nWjFYg&DbPS>bz{J)G&*l2k^2Tkzl4A_2)X`%U zlb+Z+>j?GV)F@BLGt#{_@v+QqzP3AYBCh)hkhY+ymI0;=H`&^%xR|(DGG5Dy(ue4p zB%}6VaJKG;{dBp=Iy*L@?kT_Dy8QxnC%)5;D+L5}I*3Ra?}B8ns=|CKL2SXR>_OAA zZ<^sXU{F3ktS;qCd{CIxzK5#%G$gB?VK?Nnd84^4g->ue6;Sj1^HNV6O6w^qjUf(l ziT!~W{j8X;#ElWU(HT;|2myy`#U@HYLaBxfdM=L}`}|U?+%82YdkyZgxA1g~&R~AT z&&x3n%?{ljO=k;0L{wAQ+p8he%jUJW39A?jIGM43bVzCB9@^qntGaI3z8Pb4=5;t} zFxFiU?&VBKuu%!qJriH#>SU-TnZI2OIlH2Q-}=tZ%*(zaQb|@r zs=HD_qpO>s$78LnJ)5FGcuZcE&viKaTBeXK`gWrpUw-f#uFMc%<^oe91JWCWV+rPh zxmqft7QuHN-cyXgpU^y>osBiaH`i@5$qpRp?~K)JN8UZpnxyXG?z>o)Ye0d9hqOfN z68O(AUX@Z9uae{UYs7b&RxUl6GNX_cZx=Luo)AiU@02vZrDL3s13zElm63)y);7}8 zMXNQ#``e3)ZUeY=(T`K~ccNCX%ziH5DnwtF2)5TNGB{(OPaEzxe4}Hj6P-5xjL}V| z7PPOC5)xkx4;XQ!IuDdeFXnR(oRc?A@*o&Oqgz8eY*7g*B5e_vpVuZuNbmLWHxn6( zIoq>EM;~fP>@*YK*j!*4y%ilu84V4@DU`EmmA(0IOiT<&T!*=3sC@U;?%38ImDwLE z@O$#u^s_^y)?vHVPaZmNv35tjpC{n$p;Cb~PLp5!3NL9ESxen}cKat45n1Vvd(Liq z*?EC<%gn@JgoT}gN#hiPzwL~phnrOD#H^^~_;|XN zNLXtxKQl`>%ru?;v-FaM^aiY( z?2eK^@~^8IZ(uV~FKe#njKj8<(ZCH7<$n z{QbyZ{#zdi{mJ7muRm%W6Ft5Xkl*5JC?ZKRJ+y_xkALXh#;nU7zvVLw6P@(Rpl#ug zjpxdFJ@Wr3!2A(?<$}1)b&h;yGgo6blE2^2Ya5nQ_jvb_74dU?e{pAl=R?`=#~4ab|)~E z*G)O^pOle0PZXGkt8lzNe#qalu>?L+clYjKg1GLU9p{9W1jpaswB6UN3G=URd8Xm? z)4~~bxjO*$?pC)fmRlJ$wO-?Is&Xwb(9QZi)cknYX|u=U|2V-tLczB%l%tN}j-Qun z4NShSb+=0QaLRAT3ENJvhP9AE9)=PZ-%5`e3VXUDajg$Q02dRu^` z3ai<1ne#kD=+)WU&L)A%_}-^k3W{!zte@T8SFf^bTXMKvzge#Ji01s!y^XQmN;66R z#s2nn*JgP8>fVerURkHMmoII*6OGH}ikRKn&*xr+Jc+BBN#0n*1t0Qi<>srey5|!9 z{>T;4y@jRbyM`@Yto6!Zl%dVV#@-!wFdlnL!iWPKKX;D3(mK(1?e|^675nR}59QH? ze%H8U*Iub^vnyeFyzv-9f~02i*i7H*T&|?^_yG!iWoN}z9a4 zFDu>Z!y7;U#2OKG_}!OYa9j6RL19Z)6YpZ-go(=$?+*UnLCCit{)nY;Vr7vEA94F=>0U) zED}$4zb>qSBz;_X{U)r!uNNA~SW>3=l-+t>i^O=g?TTtL(G6=8p-m9iUaYlVs) zjgx1ut5oVZev>Naw%kkm1?@87%Mt1lC_Ks}3}NO`{&%@JmKTLp{^%m=NR7_CVBR=U zjo7iU=YHr!v0nF3Le}BdXk`?uc=URmQ>wC}kpfmV6Bb88dA*(rB)Lll zd?s_;_wm@paRe`R<^Q4Utb?L#!?u4@(yc5d9nvW+B_XAV(y$&#JA7;{r5i4JHrk$yE6=P?{%NoaUI9+JnuxKFCdq8xjG#Wv^Vf61k=7< zfYX=#aqK{lK1y?w-|)^)&xp!G-4~sE=M^W51zGW3`{TX_hz@NOV1iH;5%`!v@eZz= zGz?{!y+a>Xp5A_KoB}C&(&gPQA$0k<{ z0L~b-amLLeOLo>`wie#d_y3|@Hx*aAXIh~tB! znUUt|s+L6-fL&$H{hQF7VLzUqrA}Tu8qzs^2DGjJCH4ZjIl-?UM1j~{d?3K@C5<9$ zJe@vEOky0rOEL}tkUktL;;G(W3}k5%BglQNB;Tooz97CvSglEZF3e}-;xR^rzD2O2Z~15N_v z8TzJy3^)Mf>eEGel|D>#u3#+t)K!cB)XEh9{q{zCCu6GC<8gGfO{U+aS1F7okF)iM zMoWuRC+8tu6}2~l#r$DCkIP^$GKv+li0-(l()w-gXA=M=q`4!#`?GSLiNXu9_fyaN zDCOjfx#TxvztVcOJt-+xD)=ano=b45PIn+ z=?i_h72CojbaK8@llzH}Zg>V`Vnqhwgb&+~CZzperetq}pN*o>ZQIX|=6Qfk$j19t zF_z|=hJBB$Ex&|S(yWDSY`Ru@M8;jPCAUW^zD-6oxF(VTg<+$oufrCZih`RD*2T^q zR4u(#e0Pkza(+#OY>sbt4EQy<2xrK)@SYjke3zgwt+8dh?OFPQ4E)8u-3qxZEjrRf|e>zZB4?MMlH)P z?ad#0{9=lLqdl_=V;e$&dq$K!+2C1610%YPWu_JYI_`MMON^=~lh3MN`G(grLk)d; zO}=aIRT!-g6)@I%%-M1HgvHwd1=tF{8f&hwA@wtS(j+|I?1<4@x7@b;fc@m2pf=O9 zpN6OVg2muEWX1vb^(X3`q{kA?ipQ<_e(Omu00n{TzwCJYSa7F7HpcOv_%x2aY+q2b zrUiRb&bfpwD@+lvpr4xAZ|(~z|3=K`(O+KnZl^#U!bpQu#2F{9{KA^=ozzexeJzlC z#(}dDn>3n&tV3*r`0q%;;b;|n_XIY>yiABi879;$njVMa`~67sD}$z5Qv}=H*rmWb z_UuK%fRsspS9jcol2MxxQRM&3GO;PQ-zMzZxC76(qizN>GvTguZuJ1^HELn@5|&qr zdo10sDp!gsMfa12*4rbxcP!*!FKqBwx6g3M4wbM}xUJJy{m1oI48T_MitWV(7tqEU z-?LD6Jvq;l$GoV5BQq5>G0%)yz>%lq$Sfkno|}{M6O#)ILtwz3 zG3@p%0aQw_PtBN0`_%k;h#&8AS*a$ zshp|^gPd}RZ=fG3zY3)KEbA^*Z{99Rer%K$N}bF-JC(%vVmZj^#JoFR5&Cn3EoZFv zqSyuqlk?i_ez%STb-0gwMBVEoKOMtbjvnRZroJvtjpjOPe?9T2p%BhMTQ!H8{x(D+ zF=<%iMIKR*q{Z3O?B0sLy~5qE+B@)6ssocVpA@>o6fg5y>BSWVq13ieF`|}a#SO&K z+}Y2Hl+LxW#WP!>-%7_&B}*QE_Pm4ZI1+K5Ej#})3^*!s|45R61ss4R(KN##)h!r3 z=vxSu6aD_D2I$+ih6?_B)`&C|X$e@^&55DMds;me+va0kfaSmo5j48GX0y`KtwQC2 zaElLXUUte%|4a|9DYN#DVc)mCaN4;dlDdZD@db~{+CJo4 zH9Ao_UT$#Yekwd*6Xdy-X`NRUshw$qYxuOO!Dy;C&2$JY&7JwGX{_==5(8Mfq8#2v z8w%k0@ldq8eAd%Q#;1l<%BYE$NLv{cSC5rm+>m2=LoAS>N z<{NnM<-CU#x=Spt4{1O;2nE<+Xt>+HBphq12vv%X8wrFako;T%{bIPJ}PqRxdq9#gI%NonfR z&t#G4+;aSyv+`axNeI>J>CKxMyfgt9vB3+Dypllsh0_j805HJf$QIUJ@O|d_n^$IR z2|BQQ9~s;f65O;MR<{0d^&nMadS?CgU8@Ve|G6GB3a#pJ5_|80RE83sIO?cmLqIW) z59-b}7T|c6pFCQ3_?V@|ZP^x@I8=2~sy}7$ya#X8==|hhfx_*HrV$Teu_$>E>d$OX z3-?+@KwYJg7VJsW$uT;UQ5;h{>3B8-;YE~eslWjdeMz43@;qqyZDcSdBH0pDYh_JG zPIzE>_H*7M=omjerTT&-oy?rBZxmNQzV^@BhK4n~G|rADpu)~HXm%TC zcg&MHFeTt^rs_J%mA3+mR8U20X!kwoel95dJusY=e_XFR{>oh)6SaB$^eY!gGN-+J{<_|}-w*cfVOl^-8Z#`g z97}$nZK{@@)$u7=T{9 zj2)wP5qo1PVBoP+wO!I2APh#(b03r}?0GL?EsVkf+egmG_a!2|S8fVw8vQq^{GR}H zv7+{^7ZKKo9-yD0#>Bb&4Qu9bICUy~O2pOypD&puX?R#-rwb(}h}hP*ecw|J+kh=R z(fVBhna!r_)X;aO&-_V zv-w__Ey03=p_iHmbAJ3x(+V;q4 zl=RNfus3O}tm^!)9pKpdGo}7#JjFtnj2xa0Wyx`uN3GsrW2W1RQ%SzJ!jBQY4-pRc zL~ZhEi=I?Q_X8>b)P1=z`~}`|huZ38`l}cCK3ge-;Aben^~LB>(g~NwYGg#gtO4!n zw#Vlqr<6_deWaomoAo=v1)wL*b+!~TuqVlH$90=5An0H76h z`1?RC=FuEv0PZRzFnX7@(w?=#M%I=k_#i>y{arW;=_UEBBEPzC*Z5$Z=JV zV}0$Te$RlgEM>_xfMmYAm(bU6w0S^gg^aZG9L*?!Gjnx>n^Hc^lJx|7+u&}2Xkm8tDK>Cl~ z&D}6XPL|=0kw@AXernL=hDKa2=IL@@N?^>e_0ym`X@PJi;wi9tTsl*MSX7CcL#2hC zks-|v%QF&o-)7~fN9EQXqop zPizEWX9*Nll{{MNJZ75uGx2J$s}?2ml|^#tXZ(lAt9?8_p<;&yUE8$$opi{UO0(cQ z?uZsDtAPo$E+|?~ys0)9J!w>O$b7}GVV6P0=XtQW4wsXTUzAIUEL&wK07wo+#s zcq`e?n8nc|#w!|QM_ccx0Rd2)Dj)39mOi`gtY+o<$-|ugeWVSd?9je8Lth!b`3{(E zaWmb4h4T0=Maa)3@DJ;w%YUvj)XAZb6998c5_1+rFSxj98g~9f!k=OM=F~6#w56Li zW*qy@i&u#Si^IuWXUKZ7(gM!_w`jc34vie~jWPD(1NId{JtZuuug8pt2XyvN61#RY z%bP?p2kA|M3U3$eK6|sZ+vsw=_+#+b)S6nAwsTqN)GSOQonhaO(Z`vJ^~xW4u{wu& z?kHf7n2$J{gWb=@@mcWij~)_P2RHw-pc6?wi-P`dy+hPB$|S}0$FpC!)u*u5_oVyX zMi0*e7Z6mvq2m^#adBUTZL?DBHY%G}wn={U!PdWdyAcEi`%*McTrmq^#xc^LR@`X?ZIG zUCvA+60=Fz$=tV>#wK=$r}dV@4JAM&6NAPF?p{=8u5kSP`pdmRrFS7oDj`AW$r|3M zq#;uX;fy0ZT^|Swq(P@SH845rU?HG6=sY^-^LC+qcQ@DiBdh7l-@lS6F2d!ejaM{4 z+Ytfr`Aqb1#P}Tl73;qJJO-#bA&I3fVE|wkT|OIHRIP%s{B-BppCA3H!o!%Hgu(Cx z1P0nr#%0POzm;3uVt;!YXK34X{ng;)UeIJQD40NVlxmv}&6I();3LX4jW)8T{y2aah_oPHd%O?xIJhXv1i77X`0r31@8A0uY3lIo z`NQ2G3YXmPLC8yclvH_Y_HAT){?9teCx?aT4E}w&*`I9{jBbgNx4pU`lFjLW(i@;E-1|XzlyHx ze-+(#=|Z}9w7#r%qGux%0lBjHI#OgWo&d~)sfE2d|#NJa_T9^S=Ydx zdDFnTQ!;ZcHyzrQ2{axfp)wcK^)1T1;0-lUzY>6@pg?4SIn99w_rF0|$@lg{F`|yK9uczmw!pHegnJr{2Z> zCzKhOM3>*lS6YMMk~a));G0T{R280?j4Rvsd8)mgODS}CfN;y{3^5^3>e+XAmQN=LPvCs>Hk}Wm3{Z^$IonZSY ziD`=ZCwf9YDayQevx9<=N#tdrab!# z_sxzW9y-jn4zJFbfwt=a+K!{i&Cj6LCVVdVWsBqUPaC%&JF%t&Hj7|)I#fms%q9HX z3162P2Ri*UI-0|rCcmH4EqP)cD1VZP+l0$}i9`EezevuxQhB%jx1I1aJOQ;PL@eri z^qIrMse=ihWLtaW1Nl#?(PZZgKZ?C~updaH4QLx-4WE4c?r`_)7pJ_)>{pNrpSaIT zt@v)Lr2KuT8O=HWLK=_jU16q3KQ3;FF0}e~n6)L)#V@2Z z)kq=Z?LP~8uLGr0wvT6!(pD;R19JsUJuh?N_Z}hfxnhm4jkcuS$a8S5@24D_+fh{g zGG8~3$ru>EVvgG=N6*lY_2tu-b_g4|<81Dko>)Dj!_4K&4{fO(LJ6ZhS2aKO7j>D? zYewxxb*T){iSX%{o^}t5?{O}=HSo7CTxr4qE5CBywSZ7S+3a$T>!iuo_SJ<1^01+J zH3Zqq&pxs(Uo(?HGyXI7c!Q6siy$d<%QH*)~K3J$saxDiRP@r9VjGf=tG zMcz-5w+7_NAqn#3JiAE@3HxYbyp+GgNi2NQ|BK^$j znzL-s|31if-G}x~6h=pZT(Jd1kl(zm9~QC8LG#5%bi769a&7!OOz~01`rqHy4j~@r zoh$OyEq1XTC^~C5YuE0_lQWM5u&huQKdjQH$lu|6M(D;{t9RGjr`^~V2nF!)1RHW^ zCnDqtI3a?59RP*%g#t-Jp;@@xK8|Y(4U2}YD2IwjvA-BPrrY`j>GJ{74ts?ufdl}U zyc5?{P`A~>Y)?ZH)A-M5Xf=cwuT2g}jzF;r$%>Q^{l%IIKl9oZCGDHoWU`W?p1eei z1AFqj^~XhWPOVj@a03w^R57Wth;By4fIVuN@~9ES;W~ln9hmq-DT!G>G3-}l&0uCF z?uWR-voHv{FXJrc308V^^eJ+K2uy_v9z7>B_?3Z@zHkmX;A{h~0TG>>2R6_jM`o8E z6f)6uFQ*c!;56()SFMvk7Rdm3FmB4TNiq_|2*4oexwnxM4pk~ zRQa&s-~WG$BktS3bnid0&Qrzfa=B8=S)1Q(Oi?D^>JF=xRG7O2@Lw( zz>%+IH{JbGFW$MSC=ikJJP-EpsXtcHoDpZOl3E+wpRcYt;4ILPEF3eB!m*C3LlQdp z`<1|2v(-C)#9WaZD>HgTBB&)=fVx0xAI6xq%1iM`DB)*1X-{O9gjIA zX^MYQ2&HuWg5M5#8@R@e+RQsI?qKf9ijwAd!P8%JBU>|lxsFamK|5AdtpaNYg%`ZI zjGXxhEU{GbeMw}>=+JSLWZ8Fg?)ZB_QRw-M-1$#afpYiSnd8+p`mfUJl>dilK=}{R zFg{=%Kh=i!C$JD$lma!PAdRzBy_&nUTUEiTIj++$iVPH?@i&W4=ZFGRo&DLe>$u3;>Y)h~NJhu(kqcmX zwh%vt(iS*6f-vH)-Un<`v$ua)O$(-x%e{DX3eh7R$aLloYZm8 zRNWP3c*PrCw`!2a`&#dUJp4JmwhIb(miZy)N8>}I3<=Z?$!h)`IQJ&x#hIBkH}43s zi|~HcQ^J36f;53RYh3p-4!BiAc`|a+kVA7(^IQL~_L`Bjk+%9lpan}Kbi-rEVzAMd z%iD0v!noG-TZ&){YE^ShePG+<6H)$4K~m#+-#1cB#kr?de{HTPO5PtjNQQDTz}(<+ zPF(W~8!YKOTsUjB+0!h-rmbX-pKwt*hc0c*VzBX%>ileL|O&cwQ!`83Lyn4i8)@W zR)@}ZhQ=wM(YgHF-#{Qs3W?cN*#C%PBkH{_ddZ36+&AyAU?-P`KI=hy$Kt*~oBP8t zGI%QY*0d9WM!r3;l;VF* z&3Zv#f7PJ&=`n9~rHj-EfveDY7{7c*hV<(IYkVF`WTH*d zBa%}C=TnJPBWW1QiZ6;8PLDGz&3%ST0g9TV)=Nq4^MyR7RyNEfavfAXg=8h18cy%+ zLnh`*tqSA5H)-P#<#5X;qFS;QZBWP0z**+b5qHHt#Ox#hGYI6-569vn1k5v1{}U z8z7V1Z(4hp-JW{crc2|mIuugr&4Z}6?dWgMlKYG}k7tJ8jF%nqm zXJr+*>vSLh{!d;*`roEPNT%t-d~tgN)2V&izU4b0+j#ql0vRTB!;_hm`_#NiBJra7 z+@hhA;PsBN8``&Fn%?_ZuTIIUA!HF&# z`GltVlFR!p{#`4S_I{Pi;J#Zba^dkbb9EQ){~W0BI};G`KpNcwmjLLYh8Oj$(`^8CEcOEkdlS_aD&zYFB|8E}pvK$9?1MfX2 zvn&gGAT3>NF8~mCX}z&X6Sv+`pw#9MnywTpAfg@I(CgRB5!#U7J&NhHCaL6@)1>%5 zv2&!Dlkj#iS6&@MwbF%+4z6hRWBF{OsV3KaW+G*I~}F_apSF9Y!Zfp(DRPUSK4Kg}_0bRXz=E9;b=C5}1U zfB0>_ey(_~nwl#<0(-SXp&%XkL|aw4Hf<)hX%NxvVJA7r`J2CK2#)kgkf0##5XhpLunbgqmNUo>pVnp0spHP0a_g)t!56}v2blk`48H%ntGqaUKtO2_8ho8An z%r%w;8&>_g7;@%+kmPALT`u+fXku#z~c?`^48Ia2Nr$I?~rm(Q}CM|Af*vk zFMJas&Um>iB|PF&7C)ggwhr6OvR&B~sy{l)s`><4Qs?iUC*@f^PM=9Aqre|-3IrW^ z*Sn*GCF%lXFg;Ij`}{A3g-?2x*dE#+MU2=3{&yh#KX~)?#Fu~58rPy5<;{9bhQ(7# zf7J%ggqmElOP`Jf^L4s>l2qiY((v|NWXXas{&ct!rHOd94*dj9>l8gc-{huwo|3y4 ziUt`k<}tzq78~jaf$N&qJDdEGaK!aEA#x_T1mcDV0BSStCH{FKDbq_0TBNfz!#jIV z53KI;Ja~55X!A+I@Y~*O14M4BUr(189x3Ic(q2?c-+l*a64~qiHpHYfhz`% zOqdT0a1pB*f z3nK`FIBYW#+zy%a^eGvfCH>LkQ=K@2&XDK-Pe<|=2%UjNX}0Xdm}}8Tg96Rh{B|@B z#Qw>a3p`awxlsej`L0@UEE}t^e<`!|J$}zzOkTw6NfhocALMj)egQ4~H}6=?$aHbeV}33>ToIG~g`zAnk{G{dQO4=|>?x5^)Z=!|yk;e3mYf z-=;~}RaTK{YIl+_DDXvZjX_%W)5rUDNgbDEhIaAw3J%D^S0 zqcCfzdB11;xz9*rx~RU=?q;oy@>?TA%Nw6~lbauIFFy=Q;T$qx9x@OKVKkQvmRe51 zH8(Dj>|8SI^bje(^4u9VBrq+O91J&q%fod5U`R{s%N--Ug^ zfd^E)$@NQ%n)+&QrP>CopN4d1hqg|axr@Gy@mgDgeoFcL!W{j~W)C&^1Ek$;<>~1< zGhv$clg(*8Ow3-0#C;bA-mOz9tiZ^lvG(55AK{N8MV(c5!<2N)C+-0PDxA(fy!;3;GwQD_?dNz?&*xh zqgUl!V*ver8Ry6>@FIC0{c{$PTLUkmLAvEvpj0yX7xg^r@fO)%BN}fc%gZbyc)|Ja zLH&~*q2YMcE;hYt_btRBOM;zJdnGH z_VF`_Vig={=F@8RPOF&BC`|{udD)(E22zgJAc8tEr=i!RB`P--Jd|&&hmwQVr!>`* zkF^(4CP6Ker#QxrAxFrc-o9t>Rke4l(38MiC;){2V-VxdO}1U@F@qH|F+pwm@^NFe z1BM&xPO{>02fdjP-#ux|L94vWm|QWQ1i1|SgH`;L##C|$J&4s}Jh&vIn)?o`BQk0v z@(17BA!_&Awm>7LiDI*jJkOjgyCMl5GL&BfFFrKcnFvRwb-9&;6PvX?<0X$S(-9YL zQzWqW{l}#-5UPk5;M|IZoA)Z*lsCAp1kH|wDa%W`4dpaOaC_hS;H!J2`a4}&< z0zjwDI~0KB${qz!0J)7FxF4XhvIZ-vD=ijJH3_l7CrV$3?#PkRg{e*1vSkZgp&sz{n9JkliMr>CLS zncOQ;-_pSffszNgXgJQE4a+BCHHgIHy4pknu6D&^ZFIA{(LGL?A|Fl5B@&^WhGYgk z7QP&cJVM?Lxm-3LC+0xg$-R_rBN55)Nq>ymR_wpIf&#!Ur0$UO%Q8LI#J(k#FKlSHT)Wyz-D_gC zqQ!&0k94T9Z5aotmS{$Bit-01VXnnKvov$7NVaoT_cru5j5KKUQWv#jKs zG$gH+r5`)?K2L_~21fF!MztNLfOBq|^LL%EYZg_pBi_?wA$9ml)hLD!3BKPf%3ELu z(o+1UK}kBciD`~jNXM;CySi)v>?e3YjB?cMTbe>Qn8Am8u-599Uy@5A7juh;T_%z5HYG1$8ibsWTM2ulq}3 zZ>X)Z$Q5Jm#Ql`(XU+cGeC-O7#u2qfcOed^r>oJ)Hu$S<9zf59?; zG?OK8W)3?pW29RvA2}-PqWMZf;vV+WeqnQ;(zI0F=bweGCs6)0n zk%Ms^efHGQ?};nt$dP7(aK+2THq0Yf&_#_T0YR_2G9%Uy%*(4TSol!Y(e(AI)a_?g zhX3|QGEx8Sk93U!WsZ_zb1^R9%BcDLDA`ADs>m3i5TU%@!Q-pYPVUr6#`Z_NBv0NAw!6bi^oGLRMnCKCV%Rp>xP|k*@&GMe zP~{s5OWJj>g^^+BO1It@sFsb zB*1gCSbbx5fsU7GbTf^l_Yu0H*=&qs@Rssk15Gm-GRx>)cLQd@|gxO4=ORKZ$m6t>$pH@^dLibs;9*W z-zSoK&HLq4*;D&<|Pu&-BdS5Ev;+3gAi~n-B+NHK82j zHzZ;oZnwW^Vp^c@|c<@Vtnv4NmM zeOUrQGh3J&$ly-4AMb1M6m0dqnuI%Y=D@v3rkP=WcEF96LOIEw8c0Tk(r)8PhRCbz zCE{LJ@YD*sRxt!;LiYD3ht=yz0G5qwst zQ%Ta{XF`bIxBBHD5VZqudrjXANXtY|kN7(ezBtw4roqUpO7t*DQw1(y_C0#gToMz6 z@}%kAr`s<|Qb@QS=Zt~k6}A1WO>LP$_ep^F>>L;kP|e4F+amzP2aZtuv-FRAxq6xq z7BD4+|8{6j{-b;5E>1%kUL1IaboR#mmKUVjy=hg~L`iO3KG{~~2e!-!ux$K*m;m9FTVAtdV={l{>i9lt16=qsugo>{HdrqXmYp#Ev(Fk0I0Z_ z%zQ1>5S$M8M?*elZsqC01Ai8z?>v{hg1}>y$pdsz>=SYR*RV@}Pu|O!zzTzGUmA{0 zbiRAPYukBbn<{rKOO)~IE+9o@|9@~mPx_{!XAENM*c<$Oe~dCdns~Ftqd~z>P5)v> zA$xNQ97ZzLKEva`?}lMJ63;$$gm5~cpFc&$#3PAvaA*BZT#oF8xcx|It#va;?ugb6 zENzgc{_;0efm8-tIaAB`=<#9@fnY7&_kNxuIMKI5 ze@zwNaLIq(qNjd^T{GKjaypiQWEHLQvdr{ik@b(>C=KfLWXrWZ0Og}(u!2QQe-&Gs zgyZ`mhf-C;GJ_+Q*EZ&4bgR^qlw%DIGwqZe?ZbINpz4A4ddwbQxjh~ea9DUM1C2{v z5pK!#Lfn-~8e%cZ_Tbme(q{H#QDg1X@fy@>e$%xKJ*NJ|udUjBn=-we4?q&UJqVXp zwM(-}KAG2{Hr!`9?I&!!?wh*dkad0@W4S5vjvXw|WIbp&^wgxc?Ar+A37sieQ(h=7 zTrG%yKYUhOt0ntNE3I<@p<}AsSdM$|9Aq_$@@-uw^X2ea*EB>>Dx_liJnCZqD^yONk$Ae$j*Ezw!$-7*fOA?}4T0 z=+OejEph*`qt#W%FuLQhNb=>S#M%86p_N%)$eFMa-w{8__`fp7f259|&wubtQq1)o zqEP0YyKYtm0D7Qy77O}zZI(he%KRjnxIVv7jyv2+@~t45r+M-bUcK5&!dDPqEuC%L zLMMrtO9|i&?r#_^->+x(t8j1&Do}sDuabm>yYp_M@1Y*MgRMW1*1ah*;jo!; zQPBP}t_j4O=_$9E@A85C)Xqzlwz*;&^5iATf2^)zsp`3o1*sO)N@hhZ{Z7NfU;ZNH zK>}2q-8oDWe79;Uns^w>p6bYl4xSV3^owIDj+#21qcPT5c#nO?mo}8Hv*VhNV#Vys zcektL=ZG0g)er>wU?gA)(!N-w_X^DE0`ysVfm$Ta??WmzIzshgvT-Zt&^ris85&g& zMB+)jATV=Dp035GU$LQ(Upe6h=nCpu6y${3)4t%X$OiyUHkzmrP`sgRp(V-FJAv~c zPg%M3D^K5st8{Un#dXlu;!o$28?XK5*$<)U#weK0_ig*z>LR0232B~!nBZnR#wieg z;Q*;ew5|aZd!<&}gYTmkIPyv)Pl$l^x4U5>-fjtYiAm(JUWn~ z`c20qF&_0A%^SFPlrByw2y0!TYB$NokD-|BEd z(#G=8YRmL{KDmARL4w=VA#VBMcm1@Ope{qM7GnbR3!6c)6mw4IZ;AE%UJ$%=mg%&! zQ^Zf-<=V>K#RU2H`+jMs_a76mT_0{HYl+$hg4o`Y&Wl35jE2s1nj?|I4|@sB>=W2* zXO-6`hs=L<5Xh91qu*i+q0qnUP4}Gkb+K1^M(^*I>0UFj-pvC^ZU?kFny{0H><0U) z!9+P1sM&u*r;$|u4%XLMlOAspbF|iAal@%E@}fDz0;j&eb{xry>$sdIVx3T2#p~N6 z;FXR1lwp0&_#v|QOKK`=FC-ij*&e?4q~uZnM@>I}r_!Tt34SaQp)GmCB!r|WkYmuB zE_Gln;jEcx8Mjj_J?%~YA;pQxp&0CpOnCi0#dA%H#J(a+5^g` zVc6nZdMa5&q%}iX4FDu>a!VvL_V22rJrl8bTK9VrcqymJ;~dPr=n+B+{jesTYm(vv2Cc3KF7r__9z+|FCmXNsG0h zVULErlav?A=veR=k9!)X6?ep9F1$Ozl_R;zsrBAI^8`M@{JY*zvM$uL!nw?wh~Y^! z>J?%P2wVwPCPeN9f~ZyvFzL4B17qqi?7$^1$KNpZfN7FBTSETl+DCNy-zH{cbG8_s zkOyldtGP?ja*=r0(MN4sI=EIg&cx^)#TBOC(!9QvN@Y+3?@eD$FA$5^y=cW;vKgp6 zy}eZSY=J%S>98dWk3w34=aI3fYLm{mL=zSujH@L0g8BUP6;@tJ++i>pQj{af8*jfK zXn^!pN15Tincp$dCx5e1UFsu7-CX2yf$0c-65LX!)aCFy(y^i*`lu%7?{1{Q z{@+5p7w=9sNu)QYPfAjcz4zqp5L9 z@Qt9_0}A|bM~PG(-Z0S!_y7&nxhd@UyJblq9-u9CfdR1GyIcvZNE4MO-Bkb7%y$c7IS_ADhG)R%^J~^0R(;nZ#i}cdFUJCIp!LFqLjsY98+cj`iEau1`T2 zhfweQv9lzIA1yU7$U#D`=rQ+445^C>$Sjk=KzL_+4&B+07C=h^&|XV6*#xdI$TVxl zxNDRy+!ngVJ07}wzroFhg=@Uw*_7c%mm>!uhMKi>qfHF4b~=d14YAYtK9Sa{q-=Z| z5V)FW3EX??S*eXR1}}&iO%%Hz&#NB%)e$<<>iBg+ahjnhl|>Vxp)!{Q50W+n#DDBS3mH1k5GF+~6S{W-Gj&n+6NH?9hVy>_HEM&*BnLi&k5 zV^sBP^UCXtw`vRWcDievJ(w;GL2BlJ0Hza$EmI9k1lZ?3rk{9{19F!`th zv)Wc7z}$b3Ked0+KIT;bq{7Q@5Q9KZqp|p2Gu1{amGgT$tTqypQCj8K`B*i3s*KBY zb^~F2xY{^_&{v_WL^yl#ZD*XE$gFkzC<^50>mf!idypy>YU7BVZpU8Rk?of=ei=i? zcfEaEhy&kh^Ae(NeX0-GGpc*@@3K6e=vd33aRlx6$l@N1p8mkV)?4^|7)g7c?HVER z{PndVFdu@b;s5j(a|>(&5p>jjJ}o%54qrBpjg2K0s~4-=mSj=&294c&$ok~yR0+N8zze)do#9O zO>?ayaDNiTCKC@9;Ns_nxhy~V{%=6!pO!IN!v8pBW}@cNP4)1+nlYS*4{ULdN_k^g zrvOWrsZ1{FESHujhd=MD7@WsWuzXpv|3j_NEeYK z^1$lJM-Iw5j(2!?x!?S7khkY1b+LDxg91qBe_%{4u2v?ld!hKk1s$dlby4)xwscd& zOAr$A(kV}`baH^nrIfikMhIi+(hyw%t2XB|K?qH?cHd-C%#vU^?%-GNRUhcH`8e6$YKK7tL}9s41sky1nt!!GQGhjf zz%VoTumdX$o5QckuHvAG*()`OP@)yHCqOnQ9e|-p?ROFu#d$eOpL5e2xkQDpaAdT@ z5?pK#KfHd*v#T-J#pFIHTG?+0bX{6KHqzGaR*Y2N6}(boszPk{#;F z68p;Pcz&G_xl6U_s2};$9}ae}{qdOT>4|ukOhO>wkc62`6W~OD)HZS;8-~8p2(cK$s$OChxMZSWtxyB~@-FI4C zR;gGL&e^;uUbxgr*}_6S^FM%$R6s7+V@w&%?8*o~lXxkS_qI71MhxSwxHgUQy%pR_ zRuC3kUt^fTdS$T?qZGc;K-`=$FXWmT2Cw&XrRryp&rm@kfktS6i%__2EwYW#@gz?Q zGfVlm8}_aC5at>e?)RbB%^2hA?t(yU z!&?yaUlin#NGyX6rc89r}o!xa&13lLpG$ z`CMBxO7XMkhViU!cjorDU9XH=t}sJ{jSP#*!+qQcS-Rch`@EDtJAP1yvs~1xiGcjj zxxdbvpysKLG_z~M(!4v4jGu);D!zr;5qOkpNaz5Eu0-c$lQC2GR`{M`<%4hnu*lbA zBRS{e`<%P11UckG#y`R4%PUQJEi{@8>p{r*NI(hK)psZKW*eQq0>8Tbn6nc{Qxc5T zqe9WlCcKZE5{V)*kX<*Cvygk`9f>$*M_hWHmDxO+_eIP|Q8moAjLTUzT z+5BENA9>!ul{r+%ZR#j3Ln@4r15ZEMBS?Dx$NF0h(P~}C*MU@haKa%U+4#mXc|0qO zHTfEN0))`dr=KNu(C3>=3!xQuKEGqj-65~(=ReCTg=tQN;OcJ@qB<#iej+9PaBe>l z=m2S-t=`eh9}}!@Np~sSB6qKb(le7gEl!|=JfZ6ZG9EQ%gIK72ezS<1m2&jHCrcBD z3j~lt@0VAh>|~E)_}GmNvqAc!8XDeJhIU841h(c$$^s+FPGDX$)-1aR?%fb~KVHjr z0>dRz;b%*}SA1(|-3Zv-NcC4Q5$TM#!#UJ`ztu;q&_aLuI=0>}y`U_k7ZnOs#k=LZ zybS+;biGwTl->WuJ9G*XGAK%eGzf}-z|cyGl9JNhJ+y?Nl*AB{A|>73(lLbe&^3hQ zATOCuk_ygPJG=TY&Ov`VoWGQDzY zqx|w8fARn9#>Y&I|8*7L;EE`V^UtMmL{VAdxhFFo*%EF1_5BeY5O`mFKQ!^5EPq55 zN1-+|GkntuSa{_>FM5!p%|1c%SBbLucP-AED8xtmk)W|V_r(iD5&E(qDQbLL`{|zb z3}pMq?B*~YQEX7C1j`sdJ2_LA2hTvDvO4{1)d2}rdI(8cl2OD=S&3bbNnZB1C^-UZ zLC^K2AGk*|2yTk7-^KSjuyHf~eDx!vz5o271W9TP6PTdxy2?;b+1f^Z)==@eTEUng z&N*{hfMc1)GZQUKMb*&0;fV2LAp4H%zFdteY1ze^Km*b%3>VA%Tn8 z+dnt{B%>msyisE_xcLmUP-FI(2tm?xM*r?O-p!eoqB?QgkQTWz{Qm3DyePu}qeV#_ z`_23eaqN_L@uRA`4+CZm4e6s2d2;UbUlNFvZ*T=OX-W!FN>F`KXWo4;rbAp0K4dif zt%LEPTpp>D5`&T(SrMq<5{9es!Q#{oWri*%ZgPTp$?vMsQ)9{?G&`fjNqpU7&G z?%n{hN8QZxH3w@LOV&~u$)<&WL7vZZu;iGHODW4A8L3dZpc&4k=WWnTc>)7(!k@2I z$JiFNYyFpLjdrRhz z;km~o(1qVK;`VBJ1O#7nkix8vfHF*$r87W_`0DdOphEAxjuR6T0_nsHI9hES_n}mPNVY& zCYJH6KjCK&=rhH;s=?w5kRMBVk`PSh|8*ZO{x!%^fOBU)zg+ZEKV`umMf^O-8(}+s z2nxRG4_FG=V&evnlG9^@uE6DUS%5!V4aa*g11%z!`kmnk&xw-wP;M{m8H=?~X50n< z&^zvIqv!$p;&hIMmMR|Wq~GGV&(L$R3feN^&t*g&Pz&ZHkX?(UGy4cr!PS1k(iR;I z2@#IW)y<2qQ`r$WNSXr=#@7@tey*wxpU3PAuPem7SK^wPs3QlNi{Y(z)h((@jV2__ z*G3U--&6d#buF4A;6I#i*4(ZFnNgB~Vf(UBNO*Ohil<8Fk%WG~Q{5`i=Y5wN;pS~Z zTFQFjjt!_2Y?6flG2*_27O|3+;?CAP{8Acjv}U~OVJ(9*lEN;R9pT$3=C3wxrv`6N8#SYfkEPd zZ|Fm5qQk_l$xiICAiqoP>z7+`VX@z6!lQe?`YeVqXKVS#VR0kCHe++MnSHH%!JT&m zi~_@7;aQhb$nTR73LJDE8b}F48Mq`q9vD=_`y@SN%sTvlHKrm}ypS{+M&2u7t;^t5 z`V6jK^x~)88$o%&##r_4vhN*Fjghv+eoNagg`^ia_l_q<)l6V5j(aWvG!A8tLv+po zy+TgwqJ?g8{OoAXX47={X)Ir<9Wp~ppD^3+F>hYO^Z6atRyY0{x3=pxH?R~IDD|;| z41c#96e!T9NKV>dJ7^Y;N)_tByc9_)ApSa+jp=|-`GU}V*)6!gnJ1@S81-xxoqZm7;; zBBK@W8{c!68MY)bSX`9JoI&gpeaIkT`Crez7>G&^pfei7Lp=xCuv)h_M}C#-$JSJw zjX?5~wIyZn$297|d;nmoDw*5-X&v8>&mQYxKHW z!x`S=Av!E})>61uDx8x1K=g?A~ZRCvdxP9J=J4%6*9#F`h<e&LBQN>pRW|ETiPftVistNB4&rZj4%gPfPN}e%B_}5#*PELD(D_4@((OlvML%DQB#2D-$^r0uI5?1GxH zGn+;pEvkYKNk`W42SugYuiqQyCOIxySTzvRA!}x3hU{`;{Cy!$Wv- znf{^chSF3(G8ZQpkD1~>Xp{e$dhGw9l8jBg{pIuk%fokN)j8=KgWzw~qV-IWpHVX0 zZ21*zv(2$%%)R9NOs%5vNs#)3!`q{9?=_7qtT%ZojRpA%|alr)JR6oA!9%Vg<@tHq9L%k1LZq?hr8^&?S zDz3ldlIM32QR|~?&%@?gU2G`HA1J(O3Ut3GjY;>Nzpng9lqiEewV8f)S+6KNi?ecB z5ZP$JQ}7)T(4gKDFZ@;;%*<+*Gp}>1I#2_IpPtPT|B{)EAFfRt<01fK7nN`;+Xah( zocS0d_fF&uFNP9CTR8B~akz_y>_1dM^mg9^n4ZJa&o?@2-|8Zc!`5NQfyog+5)>`x>?x@pRr%=mho*0ga8#5?k09v-n7kZ(UM;?yrF%$YE55J3`< zGZ};ny^v&3)`6jrt0C(JjBirx
  • CYzk~6GK=yaYUWspboC9*?F{O{J3>4P{YVQ* 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 z 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%0A`jAN4o(gs;3LV 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