Source code for mpas_analysis.shared.timekeeping.utility

# This software is open source software available under the BSD-3 license.
#
# Copyright (c) 2022 Triad National Security, LLC. All rights reserved.
# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights
# reserved.
# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved.
#
# Additional copyright and license information can be found in the LICENSE file
# distributed with this code, or at
# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE
"""
Time keeping utility functions
"""
# Authors
# -------
# Xylar Asay-Davis

import datetime
import netCDF4
import xarray
import numpy

from mpas_analysis.shared.timekeeping.MpasRelativeDelta import \
    MpasRelativeDelta
from mpas_analysis.shared.io.utility import decode_strings


[docs] def get_simulation_start_time(streams): """ Given a ``StreamsFile`` object, returns the simulation start time parsed from a restart file. Parameters ---------- steams : ``StreamsFile`` object For parsing an MPAS streams file Returns ------- simulation_start_time : str The start date of the simulation parsed from a restart file identified by the contents of ``streams``. Raises ------ IOError If no restart file can be found. """ # Authors # ------- # Xylar Asay-Davis try: restartFile = streams.readpath('restart')[0] except ValueError: raise IOError('No MPAS restart file found: need at least one ' 'restart file for analysis to work correctly') ds = xarray.open_dataset(restartFile) da = ds.simulationStartTime if da.dtype.type is numpy.string_: simulationStartTime = bytes.decode(da.values.tobytes()) else: simulationStartTime = da.values.tobytes() # replace underscores so it works as a CF-compliant reference date simulationStartTime = simulationStartTime.rstrip('\x00').replace('_', ' ') return simulationStartTime
[docs] def string_to_datetime(dateString): """ Given a date string and a calendar, returns a ``datetime.datetime`` Parameters ---------- dateString : string A date and time in one of the following formats:: YYYY-MM-DD hh:mm:ss YYYY-MM-DD hh.mm.ss YYYY-MM-DD SSSSS DDD hh:mm:ss DDD hh.mm.ss DDD SSSSS hh.mm.ss hh:mm:ss YYYY-MM-DD YYYY-MM SSSSS Note: either underscores or spaces can be used to separate the date from the time portion of the string. Returns ------- datetime : A ``datetime.datetime`` object Raises ------ ValueError If an invalid ``dateString`` is supplied. """ # Authors # ------- # Xylar Asay-Davis (year, month, day, hour, minute, second) = \ _parse_date_string(dateString, isInterval=False) return datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second)
[docs] def string_to_relative_delta(dateString, calendar='gregorian'): """ Given a date string and a calendar, returns an instance of ``MpasRelativeDelta`` Parameters ---------- dateString : str A date and time in one of the following formats:: YYYY-MM-DD hh:mm:ss YYYY-MM-DD hh.mm.ss YYYY-MM-DD SSSSS DDD hh:mm:ss DDD hh.mm.ss DDD SSSSS hh.mm.ss hh:mm:ss YYYY-MM-DD YYYY-MM SSSSS Note: either underscores or spaces can be used to separate the date from the time portion of the string. calendar: {'gregorian', 'noleap'}, optional The name of one of the calendars supported by MPAS cores Returns ------- relativedelta : An ``MpasRelativeDelta`` object Raises ------ ValueError If an invalid ``dateString`` is supplied. """ # Authors # ------- # Xylar Asay-Davis (years, months, days, hours, minutes, seconds) = \ _parse_date_string(dateString, isInterval=True) return MpasRelativeDelta(years=years, months=months, days=days, hours=hours, minutes=minutes, seconds=seconds, calendar=calendar)
[docs] def string_to_days_since_date(dateString, calendar='gregorian', referenceDate='0001-01-01'): """ Given a date string or an array-like of date strings, a reference date string, and a calendar, returns the number of days (as a float or numpy.array of floats) since the reference date Parameters ---------- dateStrings : str or array-like of str A date and time (or array of date/times) in one of the following formats:: YYYY-MM-DD hh:mm:ss YYYY-MM-DD hh.mm.ss YYYY-MM-DD SSSSS DDD hh:mm:ss DDD hh.mm.ss DDD SSSSS hh.mm.ss hh:mm:ss YYYY-MM-DD YYYY-MM SSSSS Note: either underscores or spaces can be used to separate the date from the time portion of the string. calendar: {'gregorian', 'noleap'}, optional The name of one of the calendars supported by MPAS cores referenceDate : str, optional A reference date of the form:: 0001-01-01 0001-01-01 00:00:00 Returns ------- days : float or numpy.array of floats The number of days since ``referenceDate`` for each date in ``dateString`` Raises ------ ValueError If an invalid ``dateString`` or ``calendar`` is supplied. """ # Authors # ------- # Xylar Asay-Davis isSingleString = isinstance(dateString, str) if isSingleString: dateString = [dateString] dates = [string_to_datetime(string) for string in dateString] days = datetime_to_days(dates, calendar=calendar, referenceDate=referenceDate) if isSingleString: days = days[0] else: days = numpy.array(days) return days
[docs] def days_to_datetime(days, calendar='gregorian', referenceDate='0001-01-01'): """ Covert days to ``datetime.datetime`` objects given a reference date and an MPAS calendar (either 'gregorian' or 'noleap'). Parameters ---------- days : float or array-like of floats The number of days since the reference date. calendar : {'gregorian', 'noleap'}, optional A calendar to be used to convert days to a ``datetime.datetime`` object. referenceDate : str, optional A reference date of the form:: 0001-01-01 0001-01-01 00:00:00 Returns ------- datetime : `datetime.datetime` (or array-like of datetimes) The days since ``referenceDate`` on the given ``calendar``. Raises ------ ValueError If an invalid ``days``, ``referenceDate`` or ``calendar`` is supplied. """ # Authors # ------- # Xylar Asay-Davis datetimes = netCDF4.num2date(days, 'days since {}'.format(referenceDate), calendar=_mpas_to_netcdf_calendar(calendar)) # convert to datetime.datetime if isinstance(datetimes, numpy.ndarray): newDateTimes = [] for date in datetimes.flat: newDateTimes.append(_round_datetime(date)) if len(newDateTimes) > 0: datetimes = numpy.reshape(numpy.array(newDateTimes), datetimes.shape) else: datetimes = _round_datetime(datetimes) return datetimes
[docs] def datetime_to_days(dates, calendar='gregorian', referenceDate='0001-01-01'): """ Given date(s), a calendar and a reference date, returns the days since the reference date, either as a single float or an array of floats. Parameters ---------- datetime : instance or array-like of datetime.datetime The date(s) to be converted to days since ``referenceDate`` on the given ``calendar``. calendar : {'gregorian', 'noleap'}, optional A calendar to be used to convert days to a ``datetime.datetime`` object. referenceDate : str, optional A reference date of the form:: 0001-01-01 0001-01-01 00:00:00 Returns ------- days : float or array of floats The days since ``referenceDate`` on the given ``calendar``. Raises ------ ValueError If an invalid ``datetimes``, ``referenceDate`` or ``calendar`` is supplied. """ # Authors # ------- # Xylar Asay-Davis isSingleDate = False if isinstance(dates, datetime.datetime): dates = [dates] isSingleDate = True days = netCDF4.date2num(dates, 'days since {}'.format(referenceDate), calendar=_mpas_to_netcdf_calendar(calendar)) if isSingleDate: days = days[0] return days
[docs] def date_to_days(year=1, month=1, day=1, hour=0, minute=0, second=0, calendar='gregorian', referenceDate='0001-01-01'): """ Convert a date to days since the reference date. Parameters ---------- year, month, day, hour, minute, second : int, optional The date to be converted to days since ``referenceDate`` on the given ``calendar``. calendar : {'gregorian', 'noleap'}, optional A calendar to be used to convert days to a ``datetime.datetime`` object. referenceDate : str, optional A reference date of the form:: 0001-01-01 0001-01-01 00:00:00 Returns ------- days : float The days since ``referenceDate`` on the given ``calendar``. Raises ------ ValueError If an invalid ``referenceDate`` or ``calendar`` is supplied. """ # Authors # ------- # Xylar Asay-Davis calendar = _mpas_to_netcdf_calendar(calendar) date = datetime.datetime(year, month, day, hour, minute, second) return netCDF4.date2num(date, 'days since {}'.format(referenceDate), calendar=calendar)
def _parse_date_string(dateString, isInterval=False): """ Given a string containing a date, returns a tuple defining a date of the form (year, month, day, hour, minute, second) appropriate for constructing a datetime or timedelta Parameters ---------- dateString : string A date and time in one of the followingformats:: YYYY-MM-DD hh:mm:ss YYYY-MM-DD hh.mm.ss YYYY-MM-DD SSSSS DDD hh:mm:ss DDD hh.mm.ss DDD SSSSS hh.mm.ss hh:mm:ss YYYY-MM-DD YYYY-MM SSSSS Note: either underscores or spaces can be used to separate the date from the time portion of the string. isInterval : bool, optional If ``isInterval=True``, the result is appropriate for constructing a `datetime.timedelta` object rather than a `datetime`. Returns ------- date : A tuple of (year, month, day, hour, minute, second) Raises ------ ValueError If an invalid ``dateString`` is supplied. """ # Authors # ------- # Xylar Asay-Davis if isInterval: offset = 0 else: offset = 1 # change underscores to spaces so both can be supported dateString = dateString.rstrip('\x00').replace('_', ' ').strip() if ' ' in dateString: ymd, hms = dateString.split(' ') else: if '-' in dateString: ymd = dateString # error can result if dateString = '1990-01' # assume this means '1990-01-01' if len(ymd.split('-')) == 2: ymd += '-01' hms = '00:00:00' else: if isInterval: ymd = '0000-00-00' else: ymd = '0001-01-01' hms = dateString if '.' in hms: hms = hms.replace('.', ':') if '-' in ymd: (year, month, day) \ = [int(sub) for sub in ymd.split('-')] else: day = int(ymd) year = 0 month = offset if ':' in hms: (hour, minute, second) \ = [int(sub) for sub in hms.split(':')] else: second = int(hms) minute = 0 hour = 0 return (year, month, day, hour, minute, second) def _mpas_to_netcdf_calendar(calendar): """ Convert from MPAS calendar to NetCDF4 calendar names. """ if calendar == 'gregorian_noleap': calendar = 'noleap' if calendar not in ['gregorian', 'noleap']: raise ValueError('Unsupported calendar {}'.format(calendar)) return calendar def _round_datetime(date): """Round a datetime object to nearest second date : datetime.datetime or similar objet object. """ (year, month, day, hour, minute, second, microsecond) = \ (date.year, date.month, date.day, date.hour, date.minute, date.second, date.microsecond) date = datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second) add_seconds = int(1e-6 * microsecond + 0.5) return date + datetime.timedelta(0, add_seconds)