From 093284f9f04af2f58ca2000b4bb57665e91b6435 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 18 May 2020 14:08:12 +0200 Subject: [PATCH] first package draft --- AUTHORS.rst | 2 +- CHANGELOG.rst | 8 +- LICENSE.txt | 2 +- README.rst | 28 ++- docs/conf.py | 31 ++-- environment.yml | 8 + requirements.txt | 17 -- setup.cfg | 14 +- src/cadati/cal_date.py | 67 +++++++ src/cadati/check_date.py | 27 +++ src/cadati/conv_doy.py | 96 ++++++++++ src/cadati/jd_date.py | 319 ++++++++++++++++++++++++++++++++++ src/cadati/np_date.py | 75 ++++++++ src/cadati/skeleton.py | 115 ------------ tests/test_conv_doy.py | 45 +++++ tests/test_date_conversion.py | 253 +++++++++++++++++++++++++++ tests/test_skeleton.py | 16 -- 17 files changed, 942 insertions(+), 181 deletions(-) create mode 100644 environment.yml delete mode 100644 requirements.txt create mode 100644 src/cadati/cal_date.py create mode 100644 src/cadati/check_date.py create mode 100644 src/cadati/conv_doy.py create mode 100644 src/cadati/jd_date.py create mode 100644 src/cadati/np_date.py delete mode 100644 src/cadati/skeleton.py create mode 100644 tests/test_conv_doy.py create mode 100644 tests/test_date_conversion.py delete mode 100644 tests/test_skeleton.py diff --git a/AUTHORS.rst b/AUTHORS.rst index a67aa1b..134dd31 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,4 +2,4 @@ Contributors ============ -* Sebastian +* Sebastian Hahn diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 226e6f5..85854aa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,9 +2,7 @@ Changelog ========= -Version 0.1 -=========== +Version 0.0.1 +============= -- Feature A added -- FIX: nasty bug #1729 fixed -- add your changes here! +- Initial package draft diff --git a/LICENSE.txt b/LICENSE.txt index 99e68fa..8776b6f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Sebastian +Copyright (c) 2020 TU Wien, Department of Geodesy and Geoinformation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index afbb869..ca2ac22 100644 --- a/README.rst +++ b/README.rst @@ -2,15 +2,35 @@ cadati ====== +.. image:: https://travis-ci.org/TUW-GEO/cadati.svg?branch=master + :target: https://travis-ci.org/TUW-GEO/cadati -Add a short description here! +.. image:: https://coveralls.io/repos/github/TUW-GEO/cadati/badge.svg?branch=master + :target: https://coveralls.io/github/TUW-GEO/cadati?branch=master +.. image:: https://badge.fury.io/py/cadati.svg + :target: http://badge.fury.io/py/cadati -Description -=========== +.. image:: https://readthedocs.org/projects/cadati/badge/?version=latest + :target: http://cadati.readthedocs.org/ -A longer description of your project goes here... +The cadati package provides CAlender, DAte and TIme functions. +Citation +======== + +- not available yet + +If you use the software in a publication then please cite it using the Zenodo DOI. Be aware that this badge links to the latest package version. + +Please select your specific version at - to get the DOI of that version. You should normally always use the DOI for the specific version of your record in citations. This is to ensure that other researchers can access the exact research artefact you used for reproducibility. + +You can find additional information regarding DOI versioning at http://help.zenodo.org/#versioning + +Contribute +========== + +We are happy if you want to contribute. Please raise an issue explaining what is missing or if you find a bug. We will also gladly accept pull requests against our master branch for new features or bug fixes. Note ==== diff --git a/docs/conf.py b/docs/conf.py index a85c299..6d24034 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,8 @@ from pkg_resources import parse_version cmd_line_template = "sphinx-apidoc -f -o {outputdir} {moduledir}" - cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir) + cmd_line = cmd_line_template.format( + outputdir=output_dir, moduledir=module_dir) args = cmd_line.split(" ") if parse_version(sphinx.__version__) >= parse_version('1.7'): @@ -82,7 +83,7 @@ # General information about the project. project = u'cadati' -copyright = u'2020, Sebastian' +copyright = u'2020, TU Wien' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -135,15 +136,15 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - 'sidebar_width': '300px', - 'page_width': '1200px' -} +# html_theme_options = { +# 'sidebar_width': '300px', +# 'page_width': '1200px' +# } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] @@ -222,21 +223,21 @@ # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -# 'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -# 'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -# 'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'user_guide.tex', u'cadati Documentation', - u'Sebastian', 'manual'), + ('index', 'user_guide.tex', u'cadati Documentation', + u'TU Wien', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..8251474 --- /dev/null +++ b/environment.yml @@ -0,0 +1,8 @@ +name: cadati_env +channels: + - default + - conda-forge +dependencies: + - numpy + - pandas + - pip diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3ce41f4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# ============================================================================= -# DEPRECATION WARNING: -# -# The file `requirements.txt` does not influence the package dependencies and -# will not be automatically created in the next version of PyScaffold (v4.x). -# -# Please have look at the docs for better alternatives -# (`Dependency Management` section). -# ============================================================================= -# -# Add your pinned requirements so that they can be easily installed with: -# pip install -r requirements.txt -# Remember to also add them in setup.cfg but unpinned. -# Example: -# numpy==1.13.3 -# scipy==1.0 -# diff --git a/setup.cfg b/setup.cfg index a8babd6..81330dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,15 +4,15 @@ [metadata] name = cadati -description = Add a short description here! -author = Sebastian -author-email = sebastian.hahn@geo.tuwien.ac.at +description = calender, data and time functions +author = TU Wien +author-email = remote.sensing@geo.tuwien.ac.at license = mit long-description = file: README.rst long-description-content-type = text/x-rst; charset=UTF-8 -url = https://github.com/pyscaffold/pyscaffold/ +url = https://www.geo.tuwien.ac.at/ project-urls = - Documentation = https://pyscaffold.org/ + Documentation = http://cadati.readthedocs.org/ # Change if running only on Windows, Mac or Linux (comma-separated) platforms = any # Add here all kinds of additional classifiers as defined under @@ -30,11 +30,11 @@ package_dir = # DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD! setup_requires = pyscaffold>=3.2a0,<3.3a0 # Add here dependencies of your project (semicolon/line-separated), e.g. -# install_requires = numpy; scipy +install_requires = numpy; scipy; pandas # The usage of test_requires is discouraged, see `Dependency Management` docs # tests_require = pytest; pytest-cov # Require a specific Python version, e.g. Python 2.7 or >= 3.4 -# python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* +python_requires = >=3.6 [options.packages.find] where = src diff --git a/src/cadati/cal_date.py b/src/cadati/cal_date.py new file mode 100644 index 0000000..b0fc061 --- /dev/null +++ b/src/cadati/cal_date.py @@ -0,0 +1,67 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module contains function to convert calendar array [Y,M,D,h,m,s,ms] into +other date formats. +""" + + +import numpy as np + +from cadati.np_date import dt2jd +from cadati.check_date import is_leap_year + +# leap year +days_past = np.array([0, 31, 60, 91, 121, 152, 182, 213, + 244, 274, 305, 335, 366]) + + +def cal2dt(cal_dt): + """ + Convert a calender array (year, month, day, hour, minute, seconds, + millisecond) to a numpy.datetime64. + + Parameters + ---------- + cal_dt : numpy.uint32 (..., 7) + calendar array with last axis representing year, month, day, hour, + minute, second, microsecond. + + Returns + ------- + dt : numpy.datetime64 + numpy datetime64 array. + """ + nonleap_years = np.invert(is_leap_year(cal_dt[..., 0])) + md = days_past[cal_dt[..., 1].astype(np.int)-1] + + rel_days = cal_dt[..., 2]*24*3600*1e3 + cal_dt[..., 3]*3600*1e3 \ + + cal_dt[..., 4]*60*1e3 + cal_dt[..., 5] * 1e3 + cal_dt[..., 6] + + fd = (md - nonleap_years + np.logical_and( + md < 60, nonleap_years)-1)*24*3600*1e3 + rel_days + + years = np.array(cal_dt[..., 0]-1970, dtype='datetime64[Y]') + dt = years + (np.array(fd, dtype='datetime64[ms]') - np.datetime64('1970')) + + return dt + + +def cal2jd(cal_dt): + """ + Convert a calender array (year, month, day, hour, minute, seconds, + millisecond) to an array of julian dates. + + Parameters + ---------- + cal_dt : numpy.uint32 (..., 7) + calendar array with last axis representing year, month, day, hour, + minute, second, microsecond. + + Returns + ------- + jd : numpy.float64 + Array of julian dates. + """ + return dt2jd(cal2dt(cal_dt)) diff --git a/src/cadati/check_date.py b/src/cadati/check_date.py new file mode 100644 index 0000000..5e5e7ac --- /dev/null +++ b/src/cadati/check_date.py @@ -0,0 +1,27 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module provides function to check dates. +""" + + +import numpy as np + + +def is_leap_year(year): + """ + Check if year is a leap year. + + Parameters + ---------- + year : numpy.ndarray or int32 + Years. + + Returns + ------- + leap_year : numpy.ndarray or boolean + Returns true if year is a leap year. + """ + return np.logical_or(np.logical_and(year % 4 == 0, year % 100 != 0), + year % 400 == 0) diff --git a/src/cadati/conv_doy.py b/src/cadati/conv_doy.py new file mode 100644 index 0000000..b6d222b --- /dev/null +++ b/src/cadati/conv_doy.py @@ -0,0 +1,96 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module provides functions related to day of year conversions. +""" + + +import numpy as np + +from cadati.jd_date import jd2cal +from cadati.np_date import dt2cal +from cadati.check_date import is_leap_year + +# leap year +days_past = np.array([0, 31, 60, 91, 121, 152, 182, 213, + 244, 274, 305, 335, 366]) + + +def doy(month, day, year=None): + """ + Calculation of day of year. If year is provided it will be tested for + leap years. + + Parameters + ---------- + month : numpy.ndarray or int32 + Month. + day : numpy.ndarray or int32 + Day. + year : numpy.ndarray or int32, optional + Year. + + Returns + ------- + day_of_year : numpy.ndarray or int32 + Day of year. + """ + day_of_year = days_past[month - 1] + day + + if year is not None: + nonleap_years = np.invert(is_leap_year(year)) + day_of_year = (day_of_year - + nonleap_years.astype('int') + + np.logical_and( + day_of_year < 60, nonleap_years).astype('int')) + + return day_of_year + + +def clim_jd2ts(clim, jd): + """ + Convert climatology array into time series array for given timestamps + (expressed as julian dates). All years are interpreted as leap + years (i.e. 1-366). + + Parameters + ---------- + clim : numpy.ndarray + Climatology array with 366 entries. + jd : numpy.float64 + Timestamps in julian dates. + + Returns + ------- + ts : numpy.ndarray + Climatology as time series for given time stamps. + """ + if clim.shape[-1] != 366: + raise ValueError('Last dimension of clim array is not 366') + + return clim[jd2cal(jd, doy_respect_nonleap_year=False)[..., 7]-1] + + +def clim_dt2ts(clim, dt): + """ + Convert climatology array into time series array for given timestamps + (expressed as julian dates). All years are interpreted as leap + years (i.e. 1-366). + + Parameters + ---------- + clim : numpy.ndarray + Climatology array with 366 entries. + dt : numpy.datetime64 + Time stamps in numpy.datetime64 dates. + + Returns + ------- + ts : numpy.ndarray + Climatology as time series for given time stamps. + """ + if clim.shape[-1] != 366: + raise ValueError('Last dimension of clim array is not 366') + + return clim[dt2cal(dt, doy_respect_nonleap_year=False)[..., 7]-1] diff --git a/src/cadati/jd_date.py b/src/cadati/jd_date.py new file mode 100644 index 0000000..aabe6fc --- /dev/null +++ b/src/cadati/jd_date.py @@ -0,0 +1,319 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module provides function to convert julian dates into other date formats. +""" + + +import datetime +import numpy as np + +from cadati.np_date import dt2cal +from cadati.check_date import is_leap_year + + +# leap year +days_past = np.array([0, 31, 60, 91, 121, 152, 182, 213, + 244, 274, 305, 335, 366]) + + +def julday(month, day, year, hour=0, minute=0, second=0): + """ + Julian date from month, day, and year. + Adapted from "Numerical Recipes in C', 2nd edition, pp. 11 + + Parameters + ---------- + month : numpy.ndarray or int32 + Month. + day : numpy.ndarray or int32 + Day. + year : numpy.ndarray or int32 + Year. + hour : numpy.ndarray or int32, optional + Hour. + minute : numpy.ndarray or int32, optional + Minute. + second : numpy.ndarray or int32, optional + Second. + + Returns + ------- + jd : numpy.ndarray or float64 + Julian day. + """ + month = np.array(month) + day = np.array(day) + + in_jan_feb = month <= 2 + jy = year - in_jan_feb + jm = month + 1 + in_jan_feb * 12 + + jd = np.int32(np.floor(365.25 * jy) + + np.floor(30.6001 * jm) + (day + 1720995.0)) + ja = np.int32(0.01 * jy) + jd += 2 - ja + np.int32(0.25 * ja) + + jd = jd + hour / 24.0 - 0.5 + minute / 1440.0 + second / 86400.0 + + return jd + + +def caldat(jd): + """ + Calendar date (month, day, year) from julian date, inverse of 'julday()' + Return value: month, day, and year in the Gregorian calendar. + + Adapted from "Numerical Recipes in C', 2nd edition, pp. 11 + + Works only for years past 1582! + + Parameters + ---------- + jd : numpy.ndarray or float64 + Julian day. + + Returns + ------- + month : numpy.ndarray or int32 + Month. + day : numpy.ndarray or int32 + Day. + year : numpy.ndarray or int32 + Year. + """ + jn = np.int32(((np.array(jd) + 0.000001).round())) + + jalpha = np.int32(((jn - 1867216) - 0.25) / 36524.25) + ja = jn + 1 + jalpha - (np.int32(0.25 * jalpha)) + jb = ja + 1524 + jc = np.int32(6680.0 + ((jb - 2439870.0) - 122.1) / 365.25) + jd2 = np.int32(365.0 * jc + (0.25 * jc)) + je = np.int32((jb - jd2) / 30.6001) + + day = jb - jd2 - np.int32(30.6001 * je) + month = je - 1 + month = (month - 1) % 12 + 1 + year = jc - 4715 + year = year - (month > 2) + + return month, day, year + + +def julian2datetime(jd, tz=None): + """ + Converts julian date to python datetime. Default is not time zone aware. + + Parameters + ---------- + julian : float64 + Julian date. + + Returns + ------- + dt : datetime + Datetime object. + """ + year, month, day, hour, minute, second, microsecond = julian2date(jd) + + if type(jd) == np.array or type(jd) == np.memmap or \ + type(jd) == np.ndarray or type(jd) == np.flatiter: + return np.array([datetime.datetime(y, m, d, h, mi, s, ms, tz) + for y, m, d, h, mi, s, ms in + zip(np.atleast_1d(year), + np.atleast_1d(month), + np.atleast_1d(day), + np.atleast_1d(hour), + np.atleast_1d(minute), + np.atleast_1d(second), + np.atleast_1d(microsecond))]) + + return datetime.datetime(year, month, day, + hour, minute, second, microsecond, tz) + + +def julian2num(jd): + """ + Convert a matplotlib date to a Julian days. + + Parameters + ---------- + jd : numpy.ndarray : int32 + Julian days. + Returns + ------- + num : numpy.ndarray : int32 + Number of days since 0001-01-01 00:00:00 UTC *plus* *one*. + """ + return jd - 1721424.5 + + +def num2julian(num): + """ + Convert a Julian days to a matplotlib date. + + Parameters + ---------- + num : numpy.ndarray : int32 + Number of days since 0001-01-01 00:00:00 UTC *plus* *one*. + Returns + ------- + jd : numpy.ndarray : int32 + Julian days. + """ + return num + 1721424.5 + + +def julian2date(jd, return_doy=False, doy_leap_year=True): + """ + Calendar date from julian date. Works only for years past 1582. + + Parameters + ---------- + jd : numpy.ndarray + Julian day. + return_doy : bool + Add day of year to output. + doy_leap_year : bool, optional + Flag if leap year has to be respected or not (default: True). + + Returns + ------- + year : numpy.ndarray + Year. + month : numpy.ndarray + Month. + day : numpy.ndarray + Day. + hour : numpy.ndarray + Hour. + minute : numpy.ndarray + Minute. + second : numpy.ndarray + Second. + day_of_year : numpy.ndarray + Day of year. Only returned when return_doy is set to True. + """ + min_julian = 2299160 + max_julian = 1827933925 + + is_single_value = False + if type(jd) in (float, int): + is_single_value = True + + julian = np.atleast_1d(np.array(jd, dtype=float)) + + if np.min(julian) < min_julian or np.max(julian) > max_julian: + raise ValueError("Value of Julian date is out of allowed range.") + + jn = (np.round(julian + 0.0000001)).astype(np.int32) + + jalpha = (((jn - 1867216) - 0.25) / 36524.25).astype(np.int32) + ja = jn + 1 + jalpha - (np.int32(0.25 * jalpha)) + jb = ja + 1524 + jc = (6680.0 + ((jb - 2439870.0) - 122.1) / 365.25).astype(np.int32) + jd2 = (365.0 * jc + (0.25 * jc)).astype(np.int32) + je = ((jb - jd2) / 30.6001).astype(np.int32) + + day = jb - jd2 - np.int64(30.6001 * je) + month = je - 1 + month = (month - 1) % 12 + 1 + year = jc - 4715 + year = year - (month > 2) + + fraction = (julian + 0.5 - jn).astype(np.float64) + eps = (np.float64(1e-12) * np.abs(jn)).astype(np.float64) + eps.clip(min=np.float64(1e-12), max=None) + hour = (fraction * 24. + eps).astype(np.int64) + hour.clip(min=0, max=23) + fraction -= hour / 24. + minute = (fraction * 1440. + eps).astype(np.int64) + minute = minute.clip(min=0, max=59) + second = (fraction - minute / 1440.) * 86400. + second = second.clip(min=0, max=None) + microsecond = ((second - np.int32(second)) * 1e6).astype(np.int32) + microsecond = microsecond.clip(min=0, max=999999) + second = second.astype(np.int32) + if is_single_value: + year, month, day, hour, minute, second, microsecond = ( + year.item(), month.item(), day.item(), hour.item(), + minute.item(), second.item(), microsecond.item()) + + if return_doy: + day_of_year = days_past[month - 1] + day + + if doy_leap_year: + nonleap_years = np.invert(is_leap_year(year)) + day_of_year = day_of_year - nonleap_years + np.logical_and( + day_of_year < 60, nonleap_years) + + if is_single_value: + day_of_year = day_of_year.item() + + return year, month, day, hour, minute, second, microsecond, day_of_year + else: + return year, month, day, hour, minute, second, microsecond + + +def jd2dt(jd): + """ + Convert array of julian dates to numpy.datetime64[ms] array. + + Parameters + ---------- + jd : numpy.float64 + Array of julian dates. + + Returns + ------- + dt : numpy.datetime64[ms] + Array of dates in datetime64[ms] format. + """ + dt = ((((jd - 2440587.5)*24.*3600.*1e6).astype('datetime64[us]') + + np.timedelta64(500, 'us')).astype('datetime64[ms]')) + + return dt + + +def jd2cal(jd, doy_respect_nonleap_year=True): + """ + Convert array of julian dates to a calendar array of year, month, day, hour, + minute, seconds, millisecond, day of year, with these quantites indexed + on the last axis. + + Parameters + ---------- + dt : numpy.ndatetime64 + numpy.ndarray of datetime64[ms] of arbitrary shape + doy_respect_nonleap_year : bool, optional + If True (default), leap years vary between 1-366 and non-leap + years 1-365. If False, doy varies between 1-366 for all years. + + Returns + ------- + cal_dt : numpy.uint32 (..., 8) + calendar array with last axis representing year, month, day, hour, + minute, second, microsecond, day of year. + """ + return dt2cal(jd2dt(jd), doy_respect_nonleap_year) + + +def jd2doy(jd, doy_respect_nonleap_year=True): + """ + Convert julian date to day of year. + + Parameters + ---------- + jd : numpy.float64 + Array of julian dates. + doy_respect_nonleap_year : bool, optional + If True (default), leap years vary between 1-366 and non-leap + years 1-365. If False, doy varies between 1-366 for all years. + + Returns + ------- + doy : numpy.int64 + Day of year. + """ + return jd2cal(jd, doy_respect_nonleap_year)[..., 7] diff --git a/src/cadati/np_date.py b/src/cadati/np_date.py new file mode 100644 index 0000000..b72fa74 --- /dev/null +++ b/src/cadati/np_date.py @@ -0,0 +1,75 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module contains function to convert numpy.datetime64 into other date formats. +""" + +import numpy as np + +from cadati.check_date import is_leap_year + +ref_dt = np.datetime64('1970-01-01') +ref_jd = 2440587.5 # julian date on 1970-01-01 00:00:00 + + +# leap year +days_past = np.array([0, 31, 60, 91, 121, 152, 182, 213, + 244, 274, 305, 335, 366]) + + +def dt2jd(dt): + """ + Convert numpy.datetime to julian dates. + + Parameters + ---------- + dt : numpy.ndatetime64 + numpy.ndarray of datetime64 + + Returns + ------- + jd : numpy.float64 + Array of julian dates. + """ + return (dt - ref_dt)/np.timedelta64(1, 'D') + ref_jd + + +def dt2cal(dt, doy_respect_nonleap_year=True): + """ + Convert array of datetime64 to a calendar array of year, month, day, hour, + minute, seconds, microsecond and day of year with these quantites indexed + on the last axis. + + Parameters + ---------- + dt : numpy.datetime64 + numpy.ndarray of datetime64[ms] of arbitrary shape + doy_respect_nonleap_year : bool, optional + If True (default), leap years vary between 1-366 and non-leap + years 1-365. If False, doy varies between 1-366 for all years. + + Returns + ------- + cal_dt : numpy.uint32 (..., 8) + calendar array with last axis representing year, month, day, hour, + minute, second, millisecond, day of year + """ + cal_dt = np.empty(dt.shape + (8,), dtype="u4") + + Y, M, D, h, m, s = [dt.astype("M8[{}]".format(x)) for x in "YMDhms"] + cal_dt[..., 0] = Y + 1970 + cal_dt[..., 1] = (M - Y) + 1 + cal_dt[..., 2] = (D - M) + 1 + cal_dt[..., 3] = (dt - D).astype("m8[h]") + cal_dt[..., 4] = (dt - h).astype("m8[m]") + cal_dt[..., 5] = (dt - m).astype("m8[s]") + cal_dt[..., 6] = (dt - s).astype("m8[ms]") + cal_dt[..., 7] = days_past[cal_dt[..., 1] - 1] + cal_dt[..., 2] + + if doy_respect_nonleap_year: + nonleap_years = np.invert(is_leap_year(cal_dt[..., 0])) + cal_dt[..., 7] = cal_dt[..., 7] - nonleap_years + np.logical_and( + cal_dt[..., 7] < 60, nonleap_years) + + return cal_dt diff --git a/src/cadati/skeleton.py b/src/cadati/skeleton.py deleted file mode 100644 index ae59660..0000000 --- a/src/cadati/skeleton.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -""" -This is a skeleton file that can serve as a starting point for a Python -console script. To run this script uncomment the following lines in the -[options.entry_points] section in setup.cfg: - - console_scripts = - fibonacci = cadati.skeleton:run - -Then run `python setup.py install` which will install the command `fibonacci` -inside your current environment. -Besides console scripts, the header (i.e. until _logger...) of this file can -also be used as template for Python modules. - -Note: This skeleton file can be safely removed if not needed! -""" - -import argparse -import sys -import logging - -from cadati import __version__ - -__author__ = "Sebastian" -__copyright__ = "Sebastian" -__license__ = "mit" - -_logger = logging.getLogger(__name__) - - -def fib(n): - """Fibonacci example function - - Args: - n (int): integer - - Returns: - int: n-th Fibonacci number - """ - assert n > 0 - a, b = 1, 1 - for i in range(n-1): - a, b = b, a+b - return a - - -def parse_args(args): - """Parse command line parameters - - Args: - args ([str]): command line parameters as list of strings - - Returns: - :obj:`argparse.Namespace`: command line parameters namespace - """ - parser = argparse.ArgumentParser( - description="Just a Fibonacci demonstration") - parser.add_argument( - "--version", - action="version", - version="cadati {ver}".format(ver=__version__)) - parser.add_argument( - dest="n", - help="n-th Fibonacci number", - type=int, - metavar="INT") - parser.add_argument( - "-v", - "--verbose", - dest="loglevel", - help="set loglevel to INFO", - action="store_const", - const=logging.INFO) - parser.add_argument( - "-vv", - "--very-verbose", - dest="loglevel", - help="set loglevel to DEBUG", - action="store_const", - const=logging.DEBUG) - return parser.parse_args(args) - - -def setup_logging(loglevel): - """Setup basic logging - - Args: - loglevel (int): minimum loglevel for emitting messages - """ - logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s" - logging.basicConfig(level=loglevel, stream=sys.stdout, - format=logformat, datefmt="%Y-%m-%d %H:%M:%S") - - -def main(args): - """Main entry point allowing external calls - - Args: - args ([str]): command line parameter list - """ - args = parse_args(args) - setup_logging(args.loglevel) - _logger.debug("Starting crazy calculations...") - print("The {}-th Fibonacci number is {}".format(args.n, fib(args.n))) - _logger.info("Script ends here") - - -def run(): - """Entry point for console_scripts - """ - main(sys.argv[1:]) - - -if __name__ == "__main__": - run() diff --git a/tests/test_conv_doy.py b/tests/test_conv_doy.py new file mode 100644 index 0000000..8b7eb41 --- /dev/null +++ b/tests/test_conv_doy.py @@ -0,0 +1,45 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module testing the different datetime conversion. +""" + +import unittest +import numpy as np +import pandas as pd + +from cadati.conv_doy import clim_jd2ts, clim_dt2ts + + +class TestClim2TsFunctions(unittest.TestCase): + + def setUp(self): + + self.clim = np.arange(1, 367) + dates = pd.date_range('2001-01-01', '2011-12-31 23:59:59', freq='1D') + self.jd = dates.to_julian_date().values + self.dt = dates.to_numpy() + + nly_clim = np.hstack((np.arange(1, 60), np.arange(61, 367))) + self.ref_ts = np.hstack((nly_clim, nly_clim, nly_clim, self.clim, + nly_clim, nly_clim, nly_clim, self.clim, + nly_clim, nly_clim, nly_clim)) + + def test_clim_jd2ts(self): + """ + Test conversion of climatology into time series using julian dates. + """ + ts = clim_jd2ts(self.clim, self.jd) + np.testing.assert_array_equal(ts, self.ref_ts) + + def test_clim_dt2ts(self): + """ + Test conversion of climatology into time series using numpy.datetime64. + """ + ts = clim_dt2ts(self.clim, self.dt) + np.testing.assert_array_equal(ts, self.ref_ts) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_date_conversion.py b/tests/test_date_conversion.py new file mode 100644 index 0000000..f1f7815 --- /dev/null +++ b/tests/test_date_conversion.py @@ -0,0 +1,253 @@ +# Copyright (c) 2020, TU Wien, Department of Geodesy and Geoinformation +# Distributed under the MIT License (see LICENSE.txt) + +""" +Module testing the different datetime conversion. +""" + +import unittest +import datetime + +import numpy as np +import pandas as pd +import numpy.testing as nptest + +from cadati.cal_date import cal2jd, cal2dt +from cadati.np_date import dt2cal, dt2jd +from cadati.jd_date import jd2cal, jd2dt, jd2doy +from cadati.jd_date import julian2datetime, julian2date, julday, caldat + + +class TestDateFunctions(unittest.TestCase): + + def setUp(self): + + years = np.arange(2000, 2012) + months = np.arange(1, 13) + days = np.arange(1, 13) + hours = np.arange(0, 12) + minutes = np.linspace(0, 55, 12, dtype=int) + seconds = np.linspace(0, 55, 12, dtype=int) + mseconds = np.linspace(0, 990, 12, dtype=int) + + self.ref_cal = np.transpose(np.vstack(( + years, months, days, hours, minutes, seconds, mseconds))) + + df = pd.DataFrame({'year': years, 'month': months, 'day': days, + 'hour': hours, 'minute': minutes, + 'second': seconds, 'ms': mseconds}) + dates = pd.DataFrame(range(len(df)), index=pd.to_datetime(df)) + + self.ref_dt = dates.index.to_numpy() + self.ref_jd = dates.index.to_julian_date().values + self.ref_doy = dates.index.dayofyear + + def test_cal2jd(self): + """ + Test convert calender array to julian dates. + """ + jd = cal2jd(self.ref_cal) + np.testing.assert_array_equal(jd, self.ref_jd) + + def test_cal2dt(self): + """ + Test convert calender array to numpy.datetime64. + """ + dt = cal2dt(self.ref_cal) + np.testing.assert_array_equal(dt, self.ref_dt) + + def test_jd2dt(self): + """ + Test convert julian dates to numpy.datetime64. + """ + dt = jd2dt(self.ref_jd) + np.testing.assert_array_equal(dt, self.ref_dt) + + def test_jd2cal(self): + """ + Test convert julian dates to calender dates. + """ + cal = jd2cal(self.ref_jd) + np.testing.assert_array_equal(cal[:, :7], self.ref_cal) + np.testing.assert_array_equal(cal[:, 7], self.ref_doy) + + def test_dt2jd(self): + """ + Test convert numpy.datetime64 to julian dates. + """ + jd = dt2jd(self.ref_dt) + np.testing.assert_array_equal(jd, self.ref_jd) + + def test_dt2cal(self): + """ + Test convert numpy.datetime64 to calender array. + """ + cal = dt2cal(self.ref_dt) + np.testing.assert_array_equal(cal[:, :7], self.ref_cal) + np.testing.assert_array_equal(cal[:, 7], self.ref_doy) + + def test_jd2doy(self): + """ + Test convert julian dates to day of year. + """ + doy = jd2doy(self.ref_jd) + np.testing.assert_array_equal(doy, self.ref_doy) + + +def test_julday(): + """ + Test julday. + """ + jd = julday(5, 25, 2016, 10, 20, 11) + jd_should = 2457533.9306828701 + nptest.assert_almost_equal(jd, jd_should) + + +def test_julday_arrays(): + """ + Test julday. + """ + jds = julday(np.array([5, 5]), + np.array([25, 25]), + np.array([2016, 2016]), + np.array([10, 10]), + np.array([20, 20]), + np.array([11, 11])) + jds_should = np.array([2457533.93068287, + 2457533.93068287]) + nptest.assert_almost_equal(jds, jds_should) + + +def test_julday_single_arrays(): + """ + Test julday. + """ + jds = julday(np.array([5]), np.array([25]), np.array([2016]), + np.array([10]), np.array([20]), np.array([11])) + + jds_should = np.array([2457533.93068287]) + nptest.assert_almost_equal(jds, jds_should) + + +def test_caldat(): + """ + Test caldat. + """ + month, day, year = caldat(2457533.93068287) + + assert month == 5 + assert day == 25 + assert year == 2016 + + +def test_caldat_array(): + """ + Test caldat. + """ + month, day, year = caldat(np.array([2457533.93068287, 2457533.93068287])) + + nptest.assert_almost_equal(month, np.array([5, 5])) + nptest.assert_almost_equal(day, np.array([25, 25])) + nptest.assert_almost_equal(year, np.array([2016, 2016])) + + +def test_julian2date(): + """ + Test julian2date. + """ + year, month, day, hour, minute, second, ms = julian2date( + 2457533.9306828701) + + assert type(year) == int + assert year == 2016 + assert month == 5 + assert day == 25 + assert hour == 10 + assert minute == 20 + assert second == 10 + assert ms == 999976 + + year, month, day, hour, minute, second, ms = julian2date( + 2454515.40972) + + assert year == 2008 + assert month == 2 + assert day == 18 + assert hour == 21 + assert minute == 49 + assert second == 59 + assert ms == 807989 + + +def test_julian2date_single_array(): + """ + Test julian2date single array. + """ + year, month, day, hour, minute, second, micro = julian2date( + np.array([2457533.9306828701])) + + assert type(year) == np.ndarray + assert year == 2016 + assert month == 5 + assert day == 25 + assert hour == 10 + assert minute == 20 + assert second == 10 + assert micro == 999976 + + +def test_julian2date_array(): + """ + Test julian2date. + """ + year, month, day, hour, minute, second, micro = julian2date( + np.array([2457533.9306828701, 2457533.9306828701])) + + nptest.assert_almost_equal(year, np.array([2016, 2016])) + nptest.assert_almost_equal(month, np.array([5, 5])) + nptest.assert_almost_equal(day, np.array([25, 25])) + nptest.assert_almost_equal(hour, np.array([10, 10])) + nptest.assert_almost_equal(minute, np.array([20, 20])) + nptest.assert_almost_equal(second, np.array([10, 10])) + nptest.assert_almost_equal(micro, np.array([999976, 999976])) + + +def test_julian2datetime(): + """ + Test julian2datetime. + """ + dt = julian2datetime(2457533.9306828701) + dt_should = datetime.datetime(2016, 5, 25, 10, 20, 10, 999976) + assert dt == dt_should + + dt = julian2datetime(2457173.8604166666) + dt_should = datetime.datetime(2015, 5, 31, 8, 39) + assert dt == dt_should + + +def test_julian2datetime_single_array(): + """ + Test julian2datetime for single array. + """ + dt = julian2datetime(np.array([2457533.9306828701])) + dt_should = np.array([datetime.datetime(2016, 5, 25, 10, 20, 10, 999976)]) + + assert type(dt) == np.ndarray + assert np.all(dt == dt_should) + + +def test_julian2datetime_array(): + """ + Test julian2datetime array conversion. + """ + dt = julian2datetime(np.array([2457533.9306828701, + 2457533.9306828701])) + dts = datetime.datetime(2016, 5, 25, 10, 20, 10, 999976) + dt_should = np.array([dts, dts]) + + assert type(dt) == np.ndarray + assert np.all(dt == dt_should) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_skeleton.py b/tests/test_skeleton.py deleted file mode 100644 index 392dd12..0000000 --- a/tests/test_skeleton.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest -from cadati.skeleton import fib - -__author__ = "Sebastian" -__copyright__ = "Sebastian" -__license__ = "mit" - - -def test_fib(): - assert fib(1) == 1 - assert fib(2) == 1 - assert fib(7) == 13 - with pytest.raises(AssertionError): - fib(-10)