From 53349e59a5daff75f236ea5595cb09c2eac0b1c0 Mon Sep 17 00:00:00 2001 From: hgoelzer Date: Fri, 24 May 2024 17:47:31 +0200 Subject: [PATCH] initial checkin of scripts --- .gitignore | 3 + ISMIP7_AIS_multigrid_generator_nc.py | 98 ++++++++++++++++++ ISMIP7_GrIS_multigrid_generat0r_nc.py | 98 ++++++++++++++++++ README.md | 23 +++++ generate_CDO_files_nc.py | 134 ++++++++++++++++++++++++ polar_stereo.py | 140 ++++++++++++++++++++++++++ wnc.py | 82 +++++++++++++++ 7 files changed, 578 insertions(+) create mode 100644 .gitignore create mode 100644 ISMIP7_AIS_multigrid_generator_nc.py create mode 100644 ISMIP7_GrIS_multigrid_generat0r_nc.py create mode 100644 generate_CDO_files_nc.py create mode 100644 polar_stereo.py create mode 100644 wnc.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9471dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.nc +__pycache__/ + diff --git a/ISMIP7_AIS_multigrid_generator_nc.py b/ISMIP7_AIS_multigrid_generator_nc.py new file mode 100644 index 0000000..8e51d71 --- /dev/null +++ b/ISMIP7_AIS_multigrid_generator_nc.py @@ -0,0 +1,98 @@ +# Write ISMIP7 grid files in CDO format + +import numpy as np +from generate_CDO_files_nc import generate_CDO_files + +# for checking +def isaninteger(x): + return np.mod(x, 1) == 0 + +##### Typically the only part a user needs to modify +# Specify various ISM grids at different resolution +rk = [16] +#rk = [32, 16, 8, 4, 2, 1] +#rk = [0.5] + +# Choose which output file to write +flag_nc = True +flag_xy = True +flag_af2 = True +##### + +# Output angle type (degrees or radians) +output_data_type = 'degrees' + +# Write additional g0 grid files +flag_g0 = False + +# Mapping information. This is EPSG 3031 for AIS +proj_info = {} +proj_info['earthradius'] = 6378137.0 +proj_info['eccentricity'] = 0.081819190842621 +proj_info['standard_parallel'] = 71. +proj_info['longitude_rot'] = 0. +proj_info['hemisphere'] = 'south' +# Offset of grid node centers. Lower left corner coordinates. +# Note sign change compared to matlab version! +proj_info['falseeasting'] = -3040000 +proj_info['falsenorthing'] = -3040000 + +# Grid dimensions of 1 km base grid +nx_base = 6081 +ny_base = 6081 + + +# g1 grid where ice thickness and SMB are defined +grids1 = [] +for r in rk: + # For any resolution but check integer grid numbers + nx = ((nx_base-1)/r)+1 + ny = ((ny_base-1)/r)+1 + if isaninteger(nx) and isaninteger(ny): + agrid = {} + agrid['dx'] = r*1000. + agrid['dy'] = r*1000. + agrid['nx'] = int(nx) + agrid['ny'] = int(ny) + agrid['offsetx'] = 0. + agrid['offsety'] = 0. + agrid['LatLonOutputFileName'] = 'grid_ISMIP7_g1_AIS_{:05d}m.nc'.format(int(r*1000)) + agrid['xyOutputFileName'] = 'xy_ISMIP7_g1_AIS_{:05d}m.nc'.format(int(r*1000)) + agrid['af2OutputFileName'] = 'af2_ISMIP7_g1_AIS_{:05d}m.nc'.format(int(r*1000)) + grids1.append(agrid) + else: + print('Warning: resolution {} km is not comensurable, skipped.'.format(r)) + +# Create grids and write out +for agrid in grids1: + #print(agrid) + success = generate_CDO_files(agrid, proj_info, output_data_type, flag_nc, flag_xy, flag_af2) + + +if flag_g0: + # g0 grid where horizontal velocities are defined e.g. for CISM + grids0 = [] + for r in rk: + # For any resolution but check integer grid numbers + nx = ((nx_base-1)/r) + ny = ((ny_base-1)/r) + if isaninteger(nx) and isaninteger(ny): + agrid = {} + agrid['dx'] = r*1000. + agrid['dy'] = r*1000. + agrid['nx'] = int(nx) + agrid['ny'] = int(ny) + # g0 grid is offset by half a grid size + agrid['offsetx'] = r*1000./2. + agrid['offsety'] = r*1000./2. + agrid['LatLonOutputFileName'] = 'grid_ISMIP7_g0_AIS_{:05d}m.nc'.format(int(r*1000)) + agrid['xyOutputFileName'] = 'xy_ISMIP7_g0_AIS_{:05d}m.nc'.format(int(r*1000)) + agrid['af2OutputFileName'] = 'af2_ISMIP7_g0_AIS_{:05d}m.nc'.format(int(r*1000)) + grids0.append(agrid) + else: + print('Warning: resolution {} km is not comensurable, skipped.'.format(r)) + + # Create grids and write out + for agrid in grids0: + #print(agrid) + success = generate_CDO_files(agrid, proj_info, output_data_type, flag_nc, flag_xy, flag_af2) diff --git a/ISMIP7_GrIS_multigrid_generat0r_nc.py b/ISMIP7_GrIS_multigrid_generat0r_nc.py new file mode 100644 index 0000000..c353ed5 --- /dev/null +++ b/ISMIP7_GrIS_multigrid_generat0r_nc.py @@ -0,0 +1,98 @@ +# Write ISMIP7 grid files in CDO format + +import numpy as np +from generate_CDO_files_nc import generate_CDO_files + +# for checking +def isaninteger(x): + return np.mod(x, 1) == 0 + +##### Typically the only part a user needs to modify +# Specify various ISM grids at different resolution +rk = [16] +#rk = [16, 8, 4, 2, 1] +#rk = [0.5] + +# Choose which output file to write +flag_nc = True +flag_xy = True +flag_af2 = True +##### + +# Output angle type (degrees or radians) +output_data_type = 'degrees' + +# Write additional g0 grid files +flag_g0 = False + +# Mapping information. This is EPSG 3413 for GrIS +proj_info = {} +proj_info['earthradius'] = 6378137.0 +proj_info['eccentricity'] = 0.081819190842621 +proj_info['standard_parallel'] = 70. +proj_info['longitude_rot'] = 315. +proj_info['hemisphere'] = 'north' +# Offset of grid node centers. Lower left corner coordinates. +# Note sign change compared to matlab version! +proj_info['falseeasting'] = -720000 +proj_info['falsenorthing'] = -3450000 + +# Grid dimensions of 1 km base grid +nx_base = 1681 +ny_base = 2881 + + +# g1 grid where ice thickness and SMB are defined +grids1 = [] +for r in rk: + # For any resolution but check integer grid numbers + nx = ((nx_base-1)/r)+1 + ny = ((ny_base-1)/r)+1 + if isaninteger(nx) and isaninteger(ny): + agrid = {} + agrid['dx'] = r*1000. + agrid['dy'] = r*1000. + agrid['nx'] = int(nx) + agrid['ny'] = int(ny) + agrid['offsetx'] = 0. + agrid['offsety'] = 0. + agrid['LatLonOutputFileName'] = 'grid_ISMIP7_g1_GrIS_{:05d}m.nc'.format(int(r*1000)) + agrid['xyOutputFileName'] = 'xy_ISMIP7_g1_GrIS_{:05d}m.nc'.format(int(r*1000)) + agrid['af2OutputFileName'] = 'af2_ISMIP7_g1_GrIS_{:05d}m.nc'.format(int(r*1000)) + grids1.append(agrid) + else: + print('Warning: resolution {} km is not comensurable, skipped.'.format(r)) + +# Create grids and write out +for agrid in grids1: + #print(agrid) + success = generate_CDO_files(agrid, proj_info, output_data_type, flag_nc, flag_xy, flag_af2) + + +if flag_g0: + # g0 grid where horizontal velocities are defined e.g. for CISM + grids0 = [] + for r in rk: + # For any resolution but check integer grid numbers + nx = ((nx_base-1)/r) + ny = ((ny_base-1)/r) + if isaninteger(nx) and isaninteger(ny): + agrid = {} + agrid['dx'] = r*1000. + agrid['dy'] = r*1000. + agrid['nx'] = int(nx) + agrid['ny'] = int(ny) + # g0 grid is offset by half a grid size + agrid['offsetx'] = r*1000./2. + agrid['offsety'] = r*1000./2. + agrid['LatLonOutputFileName'] = 'grid_ISMIP7_g0_GrIS_{:05d}m.nc'.format(int(r*1000)) + agrid['xyOutputFileName'] = 'xy_ISMIP7_g0_GrIS_{:05d}m.nc'.format(int(r*1000)) + agrid['af2OutputFileName'] = 'af2_ISMIP7_g0_GrIS_{:05d}m.nc'.format(int(r*1000)) + grids0.append(agrid) + else: + print('Warning: resolution {} km is not comensurable, skipped.'.format(r)) + + # Create grids and write out + for agrid in grids0: + #print(agrid) + success = generate_CDO_files(agrid, proj_info, output_data_type, flag_nc, flag_xy, flag_af2) diff --git a/README.md b/README.md index 32b7cc5..d374574 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # generate-ismip7-grid-files-py Python scripts to generate ISMIP7 grid description files used for CDO regridding. + +## Setup +Needs a python3 environment with +numpy, netCDF4, os + +The scripts can generate 3 different types of files for Greenland and Antarctica + +### grid description files (needed for CDO regridding) + grid_ISMIP7_g?_IS_res.nc +### xy coordinates + xy_ISMIP7_g?_IS_res.nc +### area factors + af2_ISMIP7_g?_IS_res.nc + +All files are produced for the diagnostic grid g1 (ice thickness, SMB, ..) +The scripts can also generate files for a staggered grid g0, where e.g. CISM defines horizontal velocities. + ISMIP7_AIS_multigrid_generator_nc.py + ISMIP7_GrIS_multigrid_generator_nc.py + +using + + polar_stereo.py + wnc.py diff --git a/generate_CDO_files_nc.py b/generate_CDO_files_nc.py new file mode 100644 index 0000000..62b06de --- /dev/null +++ b/generate_CDO_files_nc.py @@ -0,0 +1,134 @@ +# generate CDO lat-lon grid file from xy and mapping information +import numpy as np +import os +from wnc import wnc, wncatts +from polar_stereo import polar_xy_to_lonlat, polar_xy_scale_factor2 + +def generate_CDO_files(agrid, proj_info, output_data_type, flag_nc, flag_xy, flag_af2): + + # Grid parameters + dx = agrid['dx'] + dy = agrid['dy'] + nx = agrid['nx'] + ny = agrid['ny'] + offsetx = agrid['offsetx'] + offsety = agrid['offsety'] + + # Create gridded x and y. Dimension order is y,x! + ycenters, xcenters = np.meshgrid((np.arange(ny)*dy), (np.arange(nx)*dx), indexing='ij') + #print(xcenters.shape) + + # Generate xy corner coordinates. CDO needs bounds to rotate counterclockwise + ycorners, xcorners = np.meshgrid((np.arange(ny+1)*dy-dy/2), (np.arange(nx+1)*dx-dx/2), indexing='ij') + + ybounds = np.zeros([ycenters.shape[0], ycenters.shape[1], 4]) + + SEcorner= ycorners[:-1, 1:] + ybounds[:,:,0] = SEcorner + NEcorner= ycorners[1:, 1:] + ybounds[:,:,1] = NEcorner + NWcorner= ycorners[1:, :-1] + ybounds[:,:,2] = NWcorner + SWcorner= ycorners[:-1, :-1] + ybounds[:,:,3] = SWcorner + + xbounds = np.zeros([xcenters.shape[0], xcenters.shape[1], 4]) + + SEcorner= xcorners[:-1, 1:] + xbounds[:,:,0] = SEcorner + NEcorner= xcorners[1:, 1:] + xbounds[:,:,1] = NEcorner + NWcorner= xcorners[1:, :-1] + xbounds[:,:,2] = NWcorner + SWcorner= xcorners[:-1, :-1] + xbounds[:,:,3] = SWcorner + + # Write 2d xy netcdf file + if flag_xy: + print(f"Generating {agrid['xyOutputFileName']}") + + try: + os.remove(agrid['xyOutputFileName']) + except OSError: + pass + + # write 2D and 1D x,y + wnc(xcenters+proj_info['falseeasting']+offsetx, agrid['xyOutputFileName'], 'x2', 'm', 'grid center x-coordinate', ['y', 'x'], 0, 'NETCDF4') + wnc(ycenters+proj_info['falsenorthing']+offsety, agrid['xyOutputFileName'], 'y2', 'm', 'grid center y-coordinate', ['y', 'x'], 0, 'NETCDF4') + + wnc(xcenters[0, :]+proj_info['falseeasting']+offsetx, agrid['xyOutputFileName'], 'x1', 'm', 'grid center x-coordinate', 'x', 0, 'NETCDF4') + wnc(ycenters[:, 0]+proj_info['falsenorthing']+offsety, agrid['xyOutputFileName'], 'y1', 'm', 'grid center y-coordinate', 'y', 0, 'NETCDF4') + + # write bounds + wnc(xbounds+proj_info['falseeasting']+offsetx, agrid['xyOutputFileName'], 'x_bnds', 'm', 'grid corner x-coordinate', ['y', 'x', 'nv4'], 0, 'NETCDF4') + wnc(ybounds+proj_info['falsenorthing']+offsety, agrid['xyOutputFileName'], 'y_bnds', 'm', 'grid corner y-coordinate', ['y', 'x', 'nv4'], 0, 'NETCDF4') + + + ## Write CDO grid netcdf file + if flag_nc: + + # Create lat,lon centers + LI_grid_center_lon, LI_grid_center_lat, _ = polar_xy_to_lonlat(xcenters+proj_info['falseeasting']+offsetx, ycenters+proj_info['falsenorthing']+offsety, proj_info['standard_parallel'], proj_info['longitude_rot'], proj_info['earthradius'], proj_info['eccentricity'], proj_info['hemisphere'] ) + + # Create lat,lon bounds + LI_grid_corner_lon, LI_grid_corner_lat, _ = polar_xy_to_lonlat(xbounds+proj_info['falseeasting']+offsetx, ybounds+proj_info['falsenorthing']+offsety, proj_info['standard_parallel'], proj_info['longitude_rot'], proj_info['earthradius'], proj_info['eccentricity'], proj_info['hemisphere'] ) + + # Map to 360 range + LI_grid_center_lon = LI_grid_center_lon % 360. + LI_grid_corner_lon = LI_grid_corner_lon % 360. + + # Map to -180 to 180 range, nicer for Greenland + LI_grid_center_lon = np.where(LI_grid_center_lon < 180., LI_grid_center_lon, LI_grid_center_lon - 360.) + LI_grid_corner_lon = np.where(LI_grid_corner_lon < 180., LI_grid_corner_lon, LI_grid_corner_lon - 360.) + + # Convert to radians if requested + if output_data_type == 'radians': + LI_grid_center_lat = np.deg2rad(LI_grid_center_lat) + LI_grid_center_lon = np.deg2rad(LI_grid_center_lon) + LI_grid_corner_lat = np.deg2rad(LI_grid_corner_lat) + LI_grid_corner_lon = np.deg2rad(LI_grid_corner_lon) + + print(f"Generating {agrid['LatLonOutputFileName']}") + + try: + os.remove(agrid['LatLonOutputFileName']) + except OSError: + pass + + # grid centers + wnc(LI_grid_center_lat, agrid['LatLonOutputFileName'], 'lat', 'degrees_north', 'grid center latitude', ['y', 'x'], 0, 'NETCDF4') + wncatts(agrid['LatLonOutputFileName'],'lat','standard_name', 'latitude') + wncatts(agrid['LatLonOutputFileName'],'lat','bounds', 'lat_bnds') + + wnc(LI_grid_center_lon, agrid['LatLonOutputFileName'], 'lon', 'degrees_east', 'grid center longitude', ['y', 'x'], 0, 'NETCDF4') + wncatts(agrid['LatLonOutputFileName'],'lon','standard_name', 'longitude') + wncatts(agrid['LatLonOutputFileName'],'lon','bounds', 'lon_bnds') + + # bounds + wnc(LI_grid_corner_lat, agrid['LatLonOutputFileName'], 'lat_bnds', 'degrees_north', 'grid corner latitude', ['y', 'x', 'nv4'], 0, 'NETCDF4') + wnc(LI_grid_corner_lon, agrid['LatLonOutputFileName'], 'lon_bnds', 'degrees_east', 'grid corner longitude', ['y', 'x', 'nv4'], 0, 'NETCDF4') + + # dummy needed for mapping + wnc(np.int8(LI_grid_center_lon*0+1), agrid['LatLonOutputFileName'], 'dummy', '1', 'dummy variable', ['y', 'x'], 0, 'NETCDF4') + # add lat,lon mapping + wncatts(agrid['LatLonOutputFileName'],'dummy', 'coordinates', 'lon lat') + + ## Write af2 netcdf file + if flag_af2: + + # Create af2, lat,lon centers + LI_grid_center_af2 = polar_xy_scale_factor2(xcenters+proj_info['falseeasting']+offsetx, ycenters+proj_info['falsenorthing']+offsety, proj_info['standard_parallel'], proj_info['longitude_rot'], proj_info['earthradius'], proj_info['eccentricity'], proj_info['hemisphere']) + + print(f"Generating {agrid['af2OutputFileName']}") + + try: + os.remove(agrid['af2OutputFileName']) + except OSError: + pass + + # grid centers + wnc(LI_grid_center_af2, agrid['af2OutputFileName'], 'af2', 'scale_factor2', 'squared map scale factor', ['y', 'x'], 0, 'NETCDF4') + + + successfully_completed = True + diff --git a/polar_stereo.py b/polar_stereo.py new file mode 100644 index 0000000..51f265e --- /dev/null +++ b/polar_stereo.py @@ -0,0 +1,140 @@ +# Calculations for the polar stereographic projection +import numpy as np +import netCDF4 as nc + +def _hemi_direction(hemisphere): + """Return `1` for 'north' and `-1` for 'south'""" + return {'north': 1, 'south': -1}[hemisphere] + + +def polar_xy_to_lonlat(x, y, true_scale_lat, central_lon, re, e, hemisphere): + """Convert from Polar Stereographic (x, y) coordinates to + geodetic longitude and latitude. + + Args: + x (float): X coordinate(s) in km + y (float): Y coordinate(s) in km + true_scale_lat (float): true-scale latitude in degrees + central_lon: central_meridian, longitude offset in degrees + re (float): Earth radius in km + e (float): Earth eccentricity + hemisphere ('north' or 'south'): Northern or Southern hemisphere + + Returns: + If x and y are scalars then the result is a + two-element list containing [longitude, latitude]. + If x and y are numpy arrays then the result will be a two-element + list where the first element is a numpy array containing + the longitudes and the second element is a numpy array containing + the latitudes. + """ + + hemi_direction = _hemi_direction(hemisphere) + + g2r = np.pi/180 + + e2 = e * e + e4 = e2 ** 2 + e6 = e2 ** 3 + e8 = e2 ** 4 + slat = g2r * true_scale_lat + lambda_0 = g2r * central_lon + rho = np.sqrt(x ** 2 + y ** 2) + if abs(true_scale_lat - 90.) < 1e-5: + t = rho * np.sqrt((1 + e) ** (1 + e) * (1 - e) ** (1 - e)) / (2 * re) + else: + cm = np.cos(slat) / np.sqrt(1 - e2 * (np.sin(slat) ** 2)) + t_c = np.tan(np.pi/4.-slat/2.) / ( ((1.-e*np.sin(slat))/(1.+e*np.sin(slat)))**(e/2.) ) + t = rho * t_c / (re * cm) + + chi = (np.pi / 2) - 2 * np.arctan(t) + # from polar_convert + #phi = chi + \ + # ((e2 / 2) + (5 * e2 ** 2 / 24) + (e2 ** 3 / 12)) * np.sin(2 * chi) + \ + # ((7 * e2 ** 2 / 48) + (29 * e2 ** 3 / 240)) * np.sin(4 * chi) + \ + # (7 * e2 ** 3 / 120) * np.sin(6 * chi) + ## from snyder including e8 terms + phi = (chi + \ + (e2/2. + 5.*e4/24. + e6/12. + 13.*e8/360.) * np.sin(2.*chi) + \ + (7.*e4/48. + 29.*e6/240. + 811.*e8/11520.) * np.sin(4.*chi) + \ + (7.*e6/120. + 81.*e8/1120.) * np.sin(6.*chi) + \ + (4279.*e8/161280.) * np.sin(8.*chi)) + + lambda_ = np.arctan2(hemi_direction * x,(-hemi_direction * y)) + lambda_0 + + # Compute area factors + m = np.cos(phi)/(1 - e**2 * (np.sin(phi))**2)**0.5 + k = rho/(re*m) + # solution for pole(s), where x=0 and y=0 + poleloc = np.where((x==0) * (y==0)) + if poleloc[1].size: + #print(k[poleloc]) + k_0 = cm * ( ((1.+e)**(1.+e)) * ((1.-e)**(1.-e)))**(1./2.) / (2.*t_c) + k[poleloc] = k_0 + af2 = (1./k)**2 + + # Lat,lon + lat = phi/g2r * hemi_direction + lon = lambda_/g2r * hemi_direction + + return [lon, lat, af2] + + +def polar_xy_scale_factor2(x, y, true_scale_lat, central_lon, re, e, hemisphere): + """Calculate scale factor from Polar Stereographic (x, y) coordinates + + Args: + x (float): X coordinate(s) in km + y (float): Y coordinate(s) in km + true_scale_lat (float): true-scale latitude in degrees + central_lon: central_meridian, longitude offset in degrees ! not used + re (float): Earth radius in km + e (float): Earth eccentricity + hemisphere ('north' or 'south'): Northern or Southern hemisphere ! not used + + Returns: + If x and y are scalars then the result is a + scalar af2, the squared map scale factor. + If x and y are numpy arrays then the result will be a numpy array + containing af2, the squared map scale factor. + """ + + g2r = np.pi/180 + + e2 = e * e + e4 = e2 ** 2 + e6 = e2 ** 3 + e8 = e2 ** 4 + slat = g2r * true_scale_lat + rho = np.sqrt(x ** 2 + y ** 2) + if abs(true_scale_lat - 90.) < 1e-5: + t = rho * np.sqrt((1 + e) ** (1 + e) * (1 - e) ** (1 - e)) / (2 * re) + else: + cm = np.cos(slat) / np.sqrt(1 - e2 * (np.sin(slat) ** 2)) + t_c = np.tan(np.pi/4.-slat/2.) / ( ((1.-e*np.sin(slat))/(1.+e*np.sin(slat)))**(e/2.) ) + t = rho * t_c / (re * cm) + + chi = (np.pi / 2) - 2 * np.arctan(t) + # from polar_convert + #phi = chi + \ + # ((e2 / 2) + (5 * e2 ** 2 / 24) + (e2 ** 3 / 12)) * np.sin(2 * chi) + \ + # ((7 * e2 ** 2 / 48) + (29 * e2 ** 3 / 240)) * np.sin(4 * chi) + \ + # (7 * e2 ** 3 / 120) * np.sin(6 * chi) + ## from snyder including e8 terms + phi = (chi + \ + (e2/2. + 5.*e4/24. + e6/12. + 13.*e8/360.) * np.sin(2.*chi) + \ + (7.*e4/48. + 29.*e6/240. + 811.*e8/11520.) * np.sin(4.*chi) + \ + (7.*e6/120. + 81.*e8/1120.) * np.sin(6.*chi) + \ + (4279.*e8/161280.) * np.sin(8.*chi)) + + m = np.cos(phi)/(1 - e**2 * (np.sin(phi))**2)**0.5 + k = rho/(re*m) + # solution for pole(s), where x=0 and y=0 + poleloc = np.where((x==0) * (y==0)) + if poleloc[1].size: + #print(k[poleloc]) + k_0 = cm * ( ((1.+e)**(1.+e)) * ((1.-e)**(1.-e)))**(1./2.) / (2.*t_c) + k[poleloc] = k_0 + af2 = (1./k)**2 + + return af2 diff --git a/wnc.py b/wnc.py new file mode 100644 index 0000000..0fe4233 --- /dev/null +++ b/wnc.py @@ -0,0 +1,82 @@ +# wnc - write netcdf files and attributes +import numpy as np +import netCDF4 as nc + +def wnc(var,fname,vname,uname,lname,dnames,add_singleton_time_dim,ncformat): + # Write data from workspace to netCDF file. + # Syntax: + # wnc(var,fname,vname,uname,lname,dnames,add_singleton_time_dim,ncformat) + # var=variable array + # fname=name of netcdf file (in quotations, i.e. 'example.nc') + # vname=name of variable (also in quotations) + # uname=name of variable units (also in quotations) + # lname=long variable name + # dnames=names of dimensions + # add_singleton_time_dim=0/1 to not add/add a singleton time dimension + # ncformat=netcdf file format + + if np.ndim(var)==1: + dnames=[dnames] + nDims=1 + var_dims=len(var) + else: + nDims=np.ndim(var) + var_dims=np.shape(var) + + if len(dnames) != nDims: + raise ValueError('Dimension name list not equal in size to dimensionality of variable') + + #print(dnames) + #print(var_dims) + DimNames=[] + DimSizes=[] + for n in range(nDims): + if isinstance(var_dims, (list, tuple)): + DimNames+= [dnames[n]] + DimSizes+= [var_dims[n]] + else: + DimNames+= [dnames[n]] + DimSizes+= [var_dims] + + if add_singleton_time_dim: + # if no time dimension, add an unlimited time dimension + if not 'time' in dnames: + DimNames+= [dnames[n]] + DimSizes+= [None] + else: + raise ValueError('You requested to add unlimited time dim, but a time dim already exists.') + #print(vname) + #print(DimNames) + #print(DimSizes) + #print(var) + + ncf = nc.Dataset(fname,'a', format=ncformat) + dims = [] + for x in ncf.dimensions: + dims += [x] + #print(dims) + for n in range(len(DimNames)): + if DimNames[n] not in dims: + #print(DimNames[n]) + ncf.createDimension(DimNames[n],DimSizes[n]) + #print(ncf.dimensions) + data = ncf.createVariable(vname, var.dtype,DimNames[:]) + #print(data) + data[:] = var + ncf.variables[vname].setncattr('units', uname) + ncf.variables[vname].setncattr('long_name', lname) + ncf.close() + + +def wncatts(fname,vname,attname,value): + # Write attributes to netCDF file. + # Syntax: + # wncatts(fname,vname,att,value) + # fname = name of netcdf file (in quotations, i.e. 'example.nc') + # vname = name of variable (also in quotations) + # attname = name of attribute (also in quotations) + # value = attribute value + ncf = nc.Dataset(fname,'a') + ncf.variables[vname].setncattr(attname, value) + ncf.close() +