DataCouplerVar.py 9.85 KB
Newer Older
# Copyright 2022 - Barcelona Supercomputing Center
# Authors: Etienne Tourigny, Rodrigo Martín Posada
# MIT License
from netCDF4 import Dataset  # pylint: disable=no-name-in-module
import logging
import numpy as np
from datetime import date, timedelta

from DataCouplerUtils import days_since_refyear, filename_from_pattern
import pyoasis

class DataCouplerVar:
    def __init__(self, conf, fix_year, start_year):
        self.id = conf['id']
        self.oasis_name = conf['oasis_name']
        self.grid_name = conf['grid_name']
        self.file_pattern = conf['file_pattern']
        self.var_name = conf['netcdf_variable']
        self.ref_year = conf['yref_min']
        self.timestep = conf['timestep']
        self.scale_factor = conf['scale_factor']
        self.offset = conf['offset']
        self.min = conf['min'] if conf['min'] is not None else np.NINF
        self.max = conf['max'] if conf['max'] is not None else np.Inf
        self.interpolate = conf['interpolate']
        self.fix_year = fix_year
        # Initialization
        self.__get_grids(start_year)
        self.field = pyoasis.asarray(np.empty((2,self.grid_nx,self.grid_ny)))
        # Field that will be sent
        self.send_field = pyoasis.asarray(np.empty((self.grid_nx,self.grid_ny)))
        # Current netCDF dataset
        self.dataset = None
        # Name of the current dataset file
        self.current_file = ''
        self.time_array = None
        # Index in time_array
        self.t_index = 0
        # Dummy values
        # Timesteps [days since refdate]
        # Step before with data
        self.t1 = -9999.
        # Step after with data
        self.t2 = -9999.
        # Step at Jan 1st
        self.t0101 = -9999.
        # Step at Dec 31st
        self.t1231 = -9999.
        self.tnewyear = -9999.

    def setup(self, yy, t_global, tnewyear_global):
        # Dummy values
        self.t1 = -9999.
        self.t2 = -9999.
        self.t_index = 0
        self.t0101 = float(days_since_refyear(yy, 1, 1, self.ref_year))
        self.t1231 = float(days_since_refyear(yy, 12, 31, self.ref_year))
        # Current timestep
        t_local = self.t0101 + t_global
        self.tnewyear = self.t0101 + tnewyear_global

        logging.debug('{}: INIT - t       : {}'.format(self.id, t_local))
        logging.debug('{}: INIT - t0101   : {}'.format(self.id, self.t0101))
        logging.debug('{}: INIT - t1231   : {}'.format(self.id, self.t1231))
        logging.debug('{}: INIT - tnewyear: {}'.format(self.id, self.tnewyear))

        curr_date = date(self.ref_year, 1, 1) + timedelta(days=t_local)
        file_name = filename_from_pattern(self.file_pattern, curr_date)
        self.__open_file(file_name)
        if t_local >= self.time_array[0]:
            self.t_index = np.searchsorted(self.time_array, t_local, side='right')-1
        self.t2 = self.time_array[self.t_index]
        self.t1 = self.t2
        self.__read_file()

    def update(self, t_global):
        t_local = self.t0101 + t_global
        logging.debug('update var {} at time {}'.format(self.id, t_local))

        while t_local > self.t2:
            self.__read_next_timestep_from_files(t_local)

        var_max = self.max
        var_min = self.min
        # Monthly interpolation
        if self.timestep == 'monthly':
            if self.interpolate:
                if self.t1 < self.t2:
                    logging.debug('{}: time interpolation t1,t,t2 {} {} {}'.format(self.id, self.t1, t_local, self.t2))
                    raw_field = ((t_local-self.t1)*self.field[1] + (self.t2-t_local)*self.field[0])/(self.t2-self.t1)
                else:
                    logging.debug('{}: no time interpolation t,t2 {} {}'.format(self.id, t_local, self.t2))
                    # Hardcoded to match original Fortran amip-forcing clipping avoidance
                    if self.oasis_name == 'AMIP_sst':
                        var_min = np.NINF
                    raw_field = self.field[1]
            else:
                # Nearest neighbour
                if self.t1 < self.t2:
                    if np.floor(t_local)-np.floor(self.t1) < np.floor(self.t2)-np.floor(t_local):
                        nn = 0
                    else:
                        nn = 1
                else:
                    nn = 1
                logging.debug('{}: nearest neigbour nn,t1,t,t2 {} {} {} {}'.format(
                    self.id, nn, self.t1, t_local, self.t2))
                # Hardcoded to match Fortran amip-forcing clipping avoidance
                if self.oasis_name == 'AMIP_sst':
                    var_min = np.NINF
                raw_field = self.field[nn]
        # Daily (no interpolation)
        else:
            raw_field = self.field[1]
        # Clipping required for CMIP6 BCs
        self.send_field = np.maximum(var_min, np.minimum(var_max, raw_field))

    def finalise(self):
        self.__close_current_file()

    def __get_grids(self, start_year):
        '''
        Sets `self.grid_lat` and `self.grid_lon` from the first input netCDF file.
        '''
        yy = self.fix_year if self.fix_year > 0 else start_year
        file_name = filename_from_pattern(self.file_pattern, date(yy, 1, 1))
        ds = Dataset(file_name, 'r')
        variable_names = ds.variables.keys()

        # Look for lat and lon variables to create a 2d grid. 
        # If not found, look for sector to greate a 1d grid (only 3 sectors for co2 supported for now).
        if 'lat' in variable_names:
            lat = ds['lat'][:]
        elif 'latitude' in variable_names:
            lat = ds['latitude'][:]
        else:
            lat  = None
        if 'lon' in variable_names:
            lon = ds['lon'][:]
        elif 'longitude' in variable_names:
            lon = ds['longitude'][:]
        else:
            lon = None
        if lat is None or lon is None:
            if 'sector' in variable_names:
                num_sectors = len(ds['sector'][:])
                if num_sectors == 3:
                    self.grid_ny = 2
                    self.grid_nx = 1
                    lat=[-45,45]
                    lon=0
                    self.dy=lat[1]-lat[0]
                    self.dx=0
                    # Create 2-D grid for OASIS
                    self.grid_lon = np.tile(lon, (self.grid_ny, 1)).T
                    self.grid_lat = np.tile(lat, (self.grid_nx, 1))
                else:
                    raise Exception(
                        'DataCouplerVar: {}: Could not find sector variable of length 3. Invalid input netCDF file {}.'.format(self.id, file_name))
            else:
                raise Exception(
                    'DataCouplerVar: {}: Could not find lon or longitude variable name. Invalid input netCDF file {}.'.format(self.id, file_name))
        else:
            self.grid_ny = lat.size
            self.grid_nx = lon.size
            self.dy=lat[1]-lat[0]
            self.dx=lon[1]-lon[0]
            # Create 2-D grid for OASIS
            self.grid_lon = np.tile(lon, (self.grid_ny, 1)).T
            self.grid_lat = np.tile(lat, (self.grid_nx, 1))

        ds.close()

    def __read_next_timestep_from_files(self, t_local):
        self.t1 = self.t2
        self.field[0] = self.field[1]

        if t_local > np.max(self.time_array):
            old_file_name = self.current_file
            self.__close_current_file()
            curr_date = date(self.ref_year, 1, 1) + timedelta(days=t_local)
            file_name = filename_from_pattern(self.file_pattern, curr_date)
            if file_name == old_file_name:
                raise Exception('DataCouplerVar: {}: Could not find the next file for date {}.'.format(self.id, curr_date))
            self.__open_file(file_name)
            self.t_index = -1

        self.t_index += 1

        self.t2 = self.time_array[self.t_index]
        self.__read_file()

    def __open_file(self, file_name):
        self.current_file = file_name
        logging.debug('{}: open file  {}'.format(self.id, self.current_file))
        self.dataset = Dataset(file_name, 'r')
        self.time_array = self.dataset['time'][:]

    def __read_file(self):
        if self.fix_year > 0 and self.time_array[self.t_index] >= self.tnewyear:
            logging.debug('{}:...use first BC data for timestep {}'.format(self.id, self.time_array[self.t_index]))
            # Assign BC only if values have been assigned previously
            first_t_index = np.searchsorted(self.time_array, self.t0101, side='right')-1
            raw_field = self.dataset[self.var_name][first_t_index+1]
        elif self.fix_year > 0 and self.time_array[self.t_index] < self.t0101:
            logging.debug('{}:...use last BC data for timestep {}'.format(self.id, self.time_array[self.t_index]))
            # Assign BC only if values have been assigned previously
            last_t_index = np.searchsorted(self.time_array, self.t1231, side='right')-1
            raw_field = self.dataset[self.var_name][last_t_index]
        else:
            logging.debug('{}: ...read timestep {} {:<018}'.format(
                self.id, self.t_index, self.time_array[self.t_index]))
            raw_field = self.dataset[self.var_name][self.t_index]

        # This section can be "hardcoded" by the user
        # Hardcoded CO2 emissions data.
        logging.debug('reading {}'.format(self.var_name))
        if self.var_name == 'CO2_em_anthro' or self.var_name == 'CO2_em_AIR_anthro':
            #Read sum of all sectors / levels
            raw_field = np.sum(raw_field,axis=0)
        elif self.var_name == 'mole_fraction_of_carbon_dioxide_in_air':
            raw_field = raw_field[1:]
            logging.debug('reading co2 raw_field={} raw_field.shape={} field.shape={}'.format(str(raw_field),str(raw_field.shape),str(self.field[1].shape)))

        self.field[1] = pyoasis.asarray(np.transpose(raw_field)).astype('float64')*self.scale_factor+self.offset

    def __close_current_file(self):
        logging.debug('{}: close file {}'.format(self.id, self.current_file))
        self.dataset.close()