Source code for ocbpy.ocb_time

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2017, AGB & GC
# Full license can be found in License.md
# ----------------------------------------------------------------------------
"""Routines to convert from different file timekeeping methods to datetime."""

import datetime as dt
import numpy as np


[docs]def get_datetime_fmt_len(datetime_fmt): """Get the lenght of a string line needed for a specific datetime format. Parameters ---------- datetime_fmt : str Formatting string used to convert between datetime and string object Returns ------- str_len : int Minimum length of a string needed to hold the specified data Notes ----- See the datetime documentation for meanings of the datetime directives """ # Start by setting the base length. This accounts for any non-datetime # directives in the string length. str_len = len(datetime_fmt) # Each of the directives have character lengths that they fill. Add the # appropriate number of spaces. add_len = {'%a': 1, '%A': 10, '%b': 1, '%B': 8, '%Y': 2, '%f': 4, '%z': 3, '%Z': 1, '%j': 1, '%c': 22, '%x': 8, '%X': 7} for dt_dir in add_len.keys(): if datetime_fmt.find(dt_dir) >= 0: str_len += add_len[dt_dir] return str_len
[docs]def year_soy_to_datetime(yyyy, soy): """Converts year and soy to datetime. Parameters ---------- yyyy : int 4 digit year soy : float seconds of year Returns ------- dtime : dt.datetime datetime object """ # Calcuate doy, hour, min, seconds of day ss = soy / 86400.0 ddd = np.floor(ss) ss = (soy - ddd * 86400.0) / 3600.0 hh = np.floor(ss) ss = (soy - ddd * 86400.0 - hh * 3600.0) / 60.0 mm = np.floor(ss) ss = soy - ddd * 86400.0 - hh * 3600.0 - mm * 60.0 # Define format stime = "{:d}-{:.0f}-{:.0f}-{:.0f}-{:.0f}".format(yyyy, ddd + 1, hh, mm, ss) # Convert to datetime dtime = dt.datetime.strptime(stime, "%Y-%j-%H-%M-%S") return dtime
[docs]def yyddd_to_date(yyddd): """Convert from years since 1900 and day of year to datetime. Parameters ---------- yyddd : str String containing years since 1900 and day of year (e.g. 100126 = 2000-05-5). Returns ------- dtime : dt.datetime Datetime object containing date information """ if not isinstance(yyddd, str): raise ValueError("YYDDD must be a string") # Remove any decimal data yyddd = yyddd.split(".")[0] # Select the year year = int(yyddd[:-3]) + 1900 # Format the datetime string dtime = dt.datetime.strptime("{:d} {:s}".format(year, yyddd[-3:]), "%Y %j") return dtime
[docs]def convert_time(year=None, soy=None, yyddd=None, sod=None, date=None, tod=None, datetime_fmt="%Y-%m-%d %H:%M:%S"): """Convert to datetime from multiple time formats. Parameters ---------- year : int or NoneType Year or None if not in year-soy format (default=None) soy : int or NoneType Seconds of year or None if not in year-soy format (default=None) yyddd : str or NoneType String containing years since 1900 and 3-digit day of year (default=None) sod : int, float, or NoneType Seconds of day or None if the time of day is not in this format (default=None) date : str or NoneType String containing date information or None if not in date-time format (default=None) tod : str or NoneType String containing time of day information or None if not in date-time format (default=None) datetime_fmt : str String with the date-time or date format (default='%Y-%m-%d %H:%M:%S') Returns ------- dtime : dt.datetime Datetime object """ try: if year is not None and soy is not None: dtime = year_soy_to_datetime(year, soy) else: if yyddd is not None: ddate = yyddd_to_date(yyddd) date = ddate.strftime("%Y-%m-%d") # Ensure that the datetime format contains current date format if datetime_fmt.find("%Y-%m-%d") < 0: ifmt = datetime_fmt.upper().find("YYDDD") if ifmt >= 0: old_fmt = datetime_fmt[ifmt:ifmt + 5] datetime_fmt = datetime_fmt.replace(old_fmt, "%Y-%m-%d") else: datetime_fmt = "%Y-%m-%d {:s}".format(datetime_fmt) if tod is None: str_time = "{:}".format(date) # Ensure that the datetime format does not contain time for time_fmt in [" %H:%M:%S", " SOD"]: time_loc = datetime_fmt.upper().find(time_fmt) if time_loc > 0: datetime_fmt = datetime_fmt[:time_loc] else: str_time = "{:s} {:s}".format(date, tod) dtime = dt.datetime.strptime(str_time, datetime_fmt) if sod is not None: # Add the seconds of day to dtime microsec, sec = np.modf(sod) dtime += dt.timedelta(seconds=int(sec)) if microsec > 0.0: # Add the microseconds to dtime microsec = np.ceil(microsec * 1.0e6) dtime += dt.timedelta(microseconds=int(microsec)) except ValueError as verr: if(len(verr.args) > 0 and verr.args[0].startswith('unconverted data remains: ')): vsplit = verr.args[0].split(" ") dtime = dt.datetime.strptime(str_time[:-(len(vsplit[-1]))], datetime_fmt) else: raise ValueError(verr) return dtime
[docs]def deg2hr(lon): """Convert from degrees to hours. Parameters ---------- lon : float or array-like Longitude-like value in degrees Returns ------- lt : float or array-like Local time-like value in hours """ lon = np.asarray(lon) lt = lon / 15.0 # 12 hr/180 deg = 1/15 hr/deg return lt
[docs]def hr2deg(lt): """Convert from degrees to hours. Parameters ---------- lt : float or array-like Local time-like value in hours Returns ------- lon : float or array-like Longitude-like value in degrees """ lt = np.asarray(lt) lon = lt * 15.0 # 180 deg/12 hr = 15 deg/hr return lon
[docs]def hr2rad(lt): """Convert from hours to radians. Parameters ---------- lt : float or array-like Local time-like value in hours Returns ------- lon : float or array-like Longitude-like value in radians """ lt = np.asarray(lt) lon = lt * np.pi / 12.0 return lon
[docs]def rad2hr(lon): """Convert from radians to hours. Parameters ---------- lon : float or array-like Longitude-like value in radians Returns ------- lt : float or array-like Local time-like value in hours """ lon = np.asarray(lon) lt = lon * 12.0 / np.pi return lt
[docs]def datetime2hr(dtime): """Calculate hours of day from datetime. Parameters ---------- dtime : dt.datetime Universal time as a timestamp Returns ------- uth : float Hours of day, includes fractional hours """ uth = dtime.hour + dtime.minute / 60.0 \ + (dtime.second + dtime.microsecond * 1.0e-6) / 3600.0 return uth
[docs]def slt2glon(slt, dtime): """Convert from solar local time to geographic longitude. Parameters ---------- slt : float or array-like Solar local time in hours dtime : dt.datetime Universal time as a timestamp Returns ------- glon : float or array-like Geographic longitude in degrees """ # Calculate universal time of day in hours uth = datetime2hr(dtime) # Calculate the longitude in degrees slt = np.asarray(slt) glon = hr2deg(slt - uth) # Ensure the longitude is not at or above 360 or at or below -180 glon = fix_range(glon, -180.0, 360.0, 360.0) return glon
[docs]def glon2slt(glon, dtime): """Convert from geographic longitude to solar local time. Parameters ---------- glon : float or array-like Geographic longitude in degrees dtime : dt.datetime Universal time as a timestamp Returns ------- slt : float or array-like Solar local time in hours """ # Calculate the longitude in degrees slt = deg2hr(glon) + datetime2hr(dtime) # Ensure the local time is between 0 and 24 h slt = fix_range(slt, 0.0, 24.0) return slt
[docs]def fix_range(values, min_val, max_val, val_range=None): """Ensure cyclic values lie below the maximum and at or above the mininum. Parameters ---------- values : int, float, or array-like Values to adjust min_val : int or float Maximum that values may not meet or exceed max_val : int or float Minimum that values may not lie below val_range : int, float, or NoneType Value range or None to calculate from min and max (default=None) Returns ------- fixed_vals : int, float, or array-like Values adjusted to lie min_val <= fixed_vals < max_val """ # Cast output as array-like fixed_vals = np.asarray(values) # Test input to ensure the maximum is greater than the minimum if min_val >= max_val: raise ValueError('Minimum is not less than the maximum') # Determine the allowable range if val_range is None: val_range = max_val - min_val # Test input to ensure the value range is greater than zero if val_range <= 0.0: raise ValueError('Value range must be greater than zero') # Fix the values, allowing for deviations that are multiples of the # value range. Also propagate NaNs ibad = (np.greater_equal(fixed_vals, max_val, where=~np.isnan(fixed_vals)) & ~np.isnan(fixed_vals)) while np.any(ibad): fixed_vals[ibad] -= val_range ibad = (np.greater_equal(fixed_vals, max_val, where=~np.isnan(fixed_vals)) & ~np.isnan(fixed_vals)) ibad = (np.less(fixed_vals, min_val, where=~np.isnan(fixed_vals)) & ~np.isnan(fixed_vals)) while np.any(ibad): fixed_vals[ibad] += val_range ibad = (np.less(fixed_vals, min_val, where=~np.isnan(fixed_vals)) & ~np.isnan(fixed_vals)) return fixed_vals