def test_datetime_to_days(self): referenceDate = '0001-01-01' for calendar in ['gregorian', 'gregorian_noleap']: for dateString, expected_days in [('0001-01-01', 0.), ('0001-01-02', 1.), ('0001-02-01', 31.), ('0002-01-01', 365.)]: days = datetime_to_days(dates=string_to_datetime(dateString), calendar=calendar, referenceDate=referenceDate) self.assertEqual(days, expected_days) referenceDate = '2016-01-01' for calendar, expected_days in [('gregorian', 366.), ('gregorian_noleap', 365.)]: days = datetime_to_days(dates=string_to_datetime('2017-01-01'), calendar=calendar, referenceDate=referenceDate) self.assertEqual(days, expected_days)
def run_task(self): # {{{ ''' Computes NINO34 index and plots the time series and power spectrum with 95 and 99% confidence bounds ''' # Authors # ------- # Luke Van Roekel, Xylar Asay-Davis config = self.config calendar = self.calendar regionToPlot = config.get('indexNino34', 'region') ninoIndexNumber = regionToPlot[4:] self.logger.info("\nPlotting El Nino {} Index time series and power " "spectrum....".format(ninoIndexNumber)) self.logger.info(' Load SST data...') fieldName = 'nino' startDate = self.config.get('index', 'startDate') endDate = self.config.get('index', 'endDate') startYear = self.config.getint('index', 'startYear') endYear = self.config.getint('index', 'endYear') dataSource = config.get('indexNino34', 'observationData') observationsDirectory = build_obs_path( config, 'ocean', '{}Subdirectory'.format(fieldName)) # specify obsTitle based on data path # These are the only data sets supported if dataSource == 'HADIsst': dataPath = "{}/HADIsst_nino34_20180710.nc".format( observationsDirectory) obsTitle = 'HADSST' refDate = '1870-01-01' elif dataSource == 'ERS_SSTv4': dataPath = "{}/ERS_SSTv4_nino34_20180710.nc".format( observationsDirectory) obsTitle = 'ERS SSTv4' refDate = '1800-01-01' else: raise ValueError('Bad value for config option observationData {} ' 'in [indexNino34] section.'.format(dataSource)) mainRunName = config.get('runs', 'mainRunName') # regionIndex should correspond to NINO34 in surface weighted Average # AM regions = config.getExpression('regions', 'regions') regionToPlot = config.get('indexNino34', 'region') regionIndex = regions.index(regionToPlot) # Load data: ds = open_mpas_dataset(fileName=self.inputFile, calendar=calendar, variableList=self.variableList, startDate=startDate, endDate=endDate) # Observations have been processed to the nino34Index prior to reading dsObs = xr.open_dataset(dataPath, decode_cf=False, decode_times=False) # add the days between 0001-01-01 and the refDate so we have a new # reference date of 0001-01-01 (like for the model Time) dsObs["Time"] = dsObs.Time + \ string_to_days_since_date(dateString=refDate, calendar=calendar) nino34Obs = dsObs.sst self.logger.info( ' Compute El Nino {} Index...'.format(ninoIndexNumber)) varName = self.variableList[0] regionSST = ds[varName].isel(nOceanRegions=regionIndex) nino34Main = self._compute_nino34_index(regionSST, calendar) # Compute the observational index over the entire time range # nino34Obs = compute_nino34_index(dsObs.sst, calendar) self.logger.info( ' Computing El Nino {} power spectra...'.format(ninoIndexNumber)) spectraMain = self._compute_nino34_spectra(nino34Main) # Compute the observational spectra over the whole record spectraObs = self._compute_nino34_spectra(nino34Obs) # Compute the observational spectra over the last 30 years for # comparison. Only saving the spectra subsetEndYear = 2016 if self.controlConfig is None: subsetStartYear = 1976 else: # make the subset the same length as the input data set subsetStartYear = subsetEndYear - (endYear - startYear) time_start = datetime_to_days(datetime.datetime(subsetStartYear, 1, 1), calendar=calendar) time_end = datetime_to_days(datetime.datetime(subsetEndYear, 12, 31), calendar=calendar) nino34Subset = nino34Obs.sel(Time=slice(time_start, time_end)) spectraSubset = self._compute_nino34_spectra(nino34Subset) if self.controlConfig is None: nino34s = [nino34Obs[2:-3], nino34Subset, nino34Main[2:-3]] titles = [ '{} (Full Record)'.format(obsTitle), '{} ({} - {})'.format(obsTitle, subsetStartYear, subsetEndYear), mainRunName ] spectra = [spectraObs, spectraSubset, spectraMain] else: baseDirectory = build_config_full_path(self.controlConfig, 'output', 'timeSeriesSubdirectory') refFileName = '{}/{}.nc'.format( baseDirectory, self.mpasTimeSeriesTask.fullTaskName) dsRef = open_mpas_dataset(fileName=refFileName, calendar=calendar, variableList=self.variableList) regionSSTRef = dsRef[varName].isel(nOceanRegions=regionIndex) nino34Ref = self._compute_nino34_index(regionSSTRef, calendar) nino34s = [nino34Subset, nino34Main[2:-3], nino34Ref[2:-3]] controlRunName = self.controlConfig.get('runs', 'mainRunName') spectraRef = self._compute_nino34_spectra(nino34Ref) titles = [ '{} ({} - {})'.format(obsTitle, subsetStartYear, subsetEndYear), mainRunName, 'Control: {}'.format(controlRunName) ] spectra = [spectraSubset, spectraMain, spectraRef] # Convert frequencies to period in years for s in spectra: s['period'] = \ 1.0 / (constants.eps + s['f'] * constants.sec_per_year) self.logger.info( ' Plot El Nino {} index and spectra...'.format(ninoIndexNumber)) outFileName = '{}/nino{}_{}.png'.format(self.plotsDirectory, ninoIndexNumber, mainRunName) self._nino34_timeseries_plot( nino34s=nino34s, title=u'El Niño {} Index'.format(ninoIndexNumber), panelTitles=titles, outFileName=outFileName) self._write_xml(filePrefix='nino{}_{}'.format(ninoIndexNumber, mainRunName), plotType='Time Series', ninoIndexNumber=ninoIndexNumber) outFileName = '{}/nino{}_spectra_{}.png'.format( self.plotsDirectory, ninoIndexNumber, mainRunName) self._nino34_spectra_plot( spectra=spectra, title=u'El Niño {} power spectrum'.format(ninoIndexNumber), panelTitles=titles, outFileName=outFileName) self._write_xml(filePrefix='nino{}_spectra_{}'.format( ninoIndexNumber, mainRunName), plotType='Spectra', ninoIndexNumber=ninoIndexNumber)
def _parse_dataset_time(ds, inTimeVariableName, calendar, simulationStartTime, outTimeVariableName, referenceDate): # {{{ """ A helper function for computing a time coordinate from an MPAS time variable. Given a data set and a time variable name (or tuple of 2 time names), returns a new data set with time coordinate `outTimeVariableName` filled with days since `referenceDate` Parameters ---------- ds : xarray.DataSet object The data set containing an MPAS time variable to be used to build an xarray time coordinate. inTimeVariableName : string or tuple or list of strings The name of the time variable in the MPAS data set that will be used to build the 'Time' coordinate. The array(s) named by inTimeVariableName should contain date strings or the number of days since the start of the simulation. Typically, inTimeVariableName is one of {'daysSinceStartOfSim','xtime'}. If a list of two variable names is provided, times from the two are averaged together to determine the value of the time coordinate. In such cases, inTimeVariableName is typically {['xtime_start', 'xtime_end']}. calendar : {'gregorian', 'gregorian_noleap'} The name of one of the calendars supported by MPAS cores simulationStartTime : string The start date of the simulation, used to convert from time variables expressed as days since the start of the simulation to days since the reference date. `simulationStartTime` takes one of the following forms:: 0001-01-01 0001-01-01 00:00:00 simulationStartTime is only required if the MPAS time variable (identified by timeVariableName) is a number of days since the start of the simulation. outTimeVariableName : string The name of the coordinate to assign times to, typically 'Time'. referenceDate : string The reference date for the time variable, typically '0001-01-01', taking one of the following forms:: 0001-01-01 0001-01-01 00:00:00 Returns ------- dataset : xarray.dataset object A copy of the input data set with the `outTimeVariableName` coordinate containing the time coordinate parsed from `inTimeVariableName`. Raises ------ TypeError If the time variable has an unsupported type (not a date string or a floating-pont number of days since the start of the simulatio). ValueError If the time variable is a number of days since the start of the simulation but simulationStartTime is None. """ # Authors # ------- # Xylar Asay-Davis if isinstance(inTimeVariableName, (tuple, list)): # we want to average the two assert (len(inTimeVariableName) == 2) dsStart = _parse_dataset_time(ds=ds, inTimeVariableName=inTimeVariableName[0], calendar=calendar, simulationStartTime=simulationStartTime, outTimeVariableName=outTimeVariableName, referenceDate=referenceDate) dsEnd = _parse_dataset_time(ds=ds, inTimeVariableName=inTimeVariableName[1], calendar=calendar, simulationStartTime=simulationStartTime, outTimeVariableName=outTimeVariableName, referenceDate=referenceDate) starts = dsStart[outTimeVariableName].values ends = dsEnd[outTimeVariableName].values # replace the time in starts with the mean of starts and ends dsOut = dsStart.copy() dsOut.coords['startTime'] = (outTimeVariableName, starts) dsOut.coords['endTime'] = (outTimeVariableName, ends) dsOut.coords[outTimeVariableName] = (outTimeVariableName, [ starts[i] + (ends[i] - starts[i]) / 2 for i in range(len(starts)) ]) else: # there is just one time variable (either because we're recursively # calling the function or because we're not averaging). # The contents of the time variable is expected to be either a string # (|S64) or a float (meaning days since start of the simulation). timeVar = ds[inTimeVariableName] if timeVar.dtype == '|S64': # this is an array of date strings like 'xtime' # convert to string timeStrings = [ ''.join(str(xtime.astype('U'))).strip() for xtime in timeVar.values ] days = string_to_days_since_date(dateString=timeStrings, referenceDate=referenceDate, calendar=calendar) elif timeVar.dtype == 'float64': # this array contains floating-point days like # 'daysSinceStartOfSim' if simulationStartTime is None: raise ValueError('MPAS time variable {} appears to be a ' 'number of days since start \n' 'of sim but simulationStartTime was not' ' supplied.'.format(inTimeVariableName)) if (string_to_datetime(referenceDate) == string_to_datetime( simulationStartTime)): days = timeVar.values else: # a conversion may be required dates = days_to_datetime(days=timeVar.values, referenceDate=simulationStartTime, calendar=calendar) days = datetime_to_days(dates=dates, referenceDate=referenceDate, calendar=calendar) elif timeVar.dtype == 'timedelta64[ns]': raise TypeError('timeVar of unsupported type {}. This is likely ' 'because xarray.open_dataset \n' 'was called with decode_times=True, which can ' 'mangle MPAS times.'.format(timeVar.dtype)) else: raise TypeError("timeVar of unsupported type {}".format( timeVar.dtype)) dsOut = ds.copy() dsOut.coords[outTimeVariableName] = (outTimeVariableName, days) return dsOut # }}}
def _replicate_cycle(self, ds, dsToReplicate, calendar): # {{{ """ Replicates a periodic time series `dsToReplicate` to cover the timeframe of the dataset `ds`. Parameters ---------- ds : dataset used to find the start and end time of the replicated cycle dsToReplicate : dataset to replicate. The period of the cycle is the length of dsToReplicate plus the time between the first two time values (typically one year total). calendar : {'gregorian', 'gregorian_noleap'} The name of one of the calendars supported by MPAS cores Returns: -------- dsShift : a cyclicly repeated version of `dsToReplicte` covering the range of time of `ds`. """ # Authors # ------- # Xylar Asay-Davis, Milena Veneziani dsStartTime = days_to_datetime(ds.Time.min(), calendar=calendar) dsEndTime = days_to_datetime(ds.Time.max(), calendar=calendar) repStartTime = days_to_datetime(dsToReplicate.Time.min(), calendar=calendar) repEndTime = days_to_datetime(dsToReplicate.Time.max(), calendar=calendar) repSecondTime = days_to_datetime(dsToReplicate.Time.isel(Time=1), calendar=calendar) period = (MpasRelativeDelta(repEndTime, repStartTime) + MpasRelativeDelta(repSecondTime, repStartTime)) startIndex = 0 while(dsStartTime > repStartTime + (startIndex + 1) * period): startIndex += 1 endIndex = 0 while(dsEndTime > repEndTime + endIndex * period): endIndex += 1 dsShift = dsToReplicate.copy() times = days_to_datetime(dsShift.Time, calendar=calendar) dsShift.coords['Time'] = ('Time', datetime_to_days(times + startIndex * period, calendar=calendar)) # replicate cycle: for cycleIndex in range(startIndex, endIndex): dsNew = dsToReplicate.copy() dsNew.coords['Time'] = \ ('Time', datetime_to_days(times + (cycleIndex + 1) * period, calendar=calendar)) dsShift = xr.concat([dsShift, dsNew], dim='Time') # clip dsShift to the range of ds dsStartTime = dsShift.Time.sel(Time=ds.Time.min(), method=str('nearest')).values dsEndTime = dsShift.Time.sel(Time=ds.Time.max(), method=str('nearest')).values dsShift = dsShift.sel(Time=slice(dsStartTime, dsEndTime)) return dsShift # }}}