Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added alterative calendar support through netcdftime #21

Merged
merged 18 commits into from
Mar 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 3 additions & 23 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
# This file was autogenerated and will overwrite each time you run travis_pypi_setup.py
deploy:
provider: pypi
distributions: sdist bdist_wheel
password:
secure: !!binary |
QjBvYXY5LzJ3K2JOSUUvNllST0Z5QUJOWHpQc0ptekdkNnVubUVTb0Z6U3ZvWGpzemJsVDRKYjR1
SStEQmZNaHZzQWRzbGpTdzB0WEcvMXBKZE5wNXdiamVYSElkZnFpVXNpeTNoeXdoOWNIZzlCZ3Ji
RGsvNDI5UDBMNVAzS0lQTi9CZm5Zd1FVbG5hamZOMVQ3dnlMZmhNVkIyUS9iVXpwcXYvcm15Y2h3
MWN6YUtkSG9qYlBicFkyeG5rRzZ1NlphUmlML1JjWWkvVmk4WXBqRHo1K1dTblNTdlhKRWpydXBs
L0VMZXh4bi9hcjYyYXdWdzhtNVptb1J1Zk5ISHk1SWgzYlRmNzRsN25GU29BYWRHeXUyelZoQWRF
YVdLT1lOdGpXa3BmS0VMVitOLzJPY1BWYnZmanJRUng2bStZMThnQ2FQVGw4K3lGdnZpcHgvaXJ6
dUFUTTJVNFNkZVNvNnA1Uk9KZXg1MlZ1RFFWRTgvSXJXSGpMejg5M05QUnpvUER1REhueHpuTXFW
bDY5TWhsa3o3ZUE3SEcvYmxVMW9PMkpRb1FuakJjNDA0UFdmZ3BPZzhZd2MwR1h1N2dBUWRkUFN6
bW9ISk94NHZIRG1MN0lST0xDQVdPNzhMV0xxWmkwTjJLZmtOZ0pvWCtJOEJGYVRWWWtLV2JhYjEy
NVc5SVpTVVVzY3VDd09wV2ZMcTVSZXNMQWRVSHdyM01ZYVlpNVdYUmo3NU5nMlNKMjVnVVIrakNp
a2xmbXA3cDF3T1pQWjVuMjhKOWVVS1hvaUUzc2JoZFdEM3Q3N2poZ3NjMys3Sk1MdVU2MzFLNElE
MS8wRWRMcXk2SENrbEsrQ1h1WTR5bmxzLzFDbm5JSGRCUFd3U3pBSUxleUtzV2ptV1pxaUhzQW89
user: Twine
true:
condition: $TOXENV == py27
repo: mcgibbon/sympl
tags: true
env:
- TOXENV=cov
- TOXENV=flake8
- TOXENV=py35
- TOXENV=py27
install: pip install -U tox
install:
- pip install -U tox
language: python
notifications:
email:
recipients:
- mcgibbon@uw.edu
- joy.monteiro@misu.su.se
on_success: change
python: 3.5
script: tox -e ${TOXENV}
10 changes: 10 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ What's New
Latest
------

No changes yet!

v0.3.0
------

* Modified component class checking to look at the presence of properties
* Added ScalingWrapper
* Fixed bug in TendencyInDiagnosticsWrapper where tendency_diagnostics_properties were
Expand All @@ -29,6 +34,11 @@ Latest
Implicit.
* Added a function reset_constants to reset the constants library to its
initial state.
* Added a function datetime which accepts calendar as a keyword argument, and
returns datetimes from netcdftime when non-default calendars are used. The
dependency on netcdftime is optional, the other calendars just won't work if
it isn't installed
* Added a reference to the built-in timedelta for convenience.

Breaking changes
~~~~~~~~~~~~~~~~
Expand Down
8 changes: 8 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
How to release, in case you forget:

git pull to make sure you have all changes
update HISTORY.rst with the new version
bumpversion [major/minor/patch] will increase version, commit, and make a new tag
git push origin <tagname> tags must be manually pushed to github
python setup.py sdist upload -r pypi
Add a new "Latest" header to HISTORY.rst, commit, and push
5 changes: 5 additions & 0 deletions docs/computation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,9 @@ code to compute the value at the next timestep, and write an
developers of Sympl if you would like advice on your specific situation! We're
always excited about new wrapped components.

.. autoclass:: sympl.ImplicitPrognostic
:members:
:special-members:
:exclude-members: __weakref__,__metaclass__

.. _Python documentation for dicts: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
2 changes: 1 addition & 1 deletion docs/constants.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ allow you to set constants regardless of whether a value is already defined
for that constant, allowing you to add new constants we haven't thought of.

The constant library can be reverted to its original state when Sympl is
imported by calling `~sympl.reset_constants`.
imported by calling :py:func:`~sympl.reset_constants`.

.. autofunction:: sympl.get_constant

Expand Down
43 changes: 27 additions & 16 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ will be looking at:
get_initial_state, Radiation, BoundaryLayer, DeepConvection,
ImplicitDynamics)
from sympl import (
AdamsBashforth, PlotFunctionMonitor, UpdateFrequencyWrapper)
from datetime import datetime, timedelta
AdamsBashforth, PlotFunctionMonitor, UpdateFrequencyWrapper,
datetime, timedelta)

def my_plot_function(fig, state):
ax = fig.add_subplot(1, 1, 1)
Expand All @@ -31,20 +31,20 @@ will be looking at:
state = get_initial_state(nx=256, ny=128, nz=64)
state['time'] = datetime(2000, 1, 1)

physics_stepper = AdamsBashforth(
physics_stepper = AdamsBashforth([
UpdateFrequencyWrapper(Radiation(), timedelta(hours=2)),
BoundaryLayer(),
DeepConvection(),
)
])
implicit_dynamics = ImplicitDynamics()

timestep = timedelta(minutes=30)
while state['time'] < datetime(2010, 1, 1):
physics_diagnostics, state_after_physics = physics_stepper(state, timestep)
dynamics_diagnostics, next_state = implicit_dynamics(state_after_physics, timestep)
state_after_physics.update(physics_diagnostics)
state_after_physics.update(dynamics_diagnostics)
plot_monitor.store(state_after_physics)
state.update(physics_diagnostics)
state.update(dynamics_diagnostics)
plot_monitor.store(state)
next_state['time'] = state['time'] + timestep
state = next_state

Expand All @@ -59,8 +59,8 @@ At the beginning of the script we have import statements:
get_initial_state, Radiation, BoundaryLayer, DeepConvection,
ImplicitDynamics)
from sympl import (
AdamsBashforth, PlotFunctionMonitor, UpdateFrequencyWrapper)
from datetime import datetime, timedelta
AdamsBashforth, PlotFunctionMonitor, UpdateFrequencyWrapper,
datetime, timedelta)

These grant access to the objects that will be used to construct the model,
and are dependent on the model package you are using. Here, the names
Expand Down Expand Up @@ -113,7 +113,14 @@ An initialized `state` is a dictionary whose keys are strings (like
'air_temperature') and values are :py:class:`~sympl.DataArray` objects, which
store not only the data but also metadata like units. The one exception
is the "time" quantity which is either a `datetime`-like or `timedelta`-like
object. You can read more about the `state` in :ref:`Model State`.
object. Here we are calling :py:func:`sympl.datetime` to initialize time,
rather than directly creating a Python datetime. This is because
:py:func:`sympl.datetime` can support a number of calendars using the
`netcdftime` package, if installed, unlike the built-in `datetime` which only
supports the Proleptic Gregorian calendar.

You can read more about the `state`, including :py:func:`sympl.datetime` in
:ref:`Model State`.

Initialize Components
---------------------
Expand All @@ -123,11 +130,11 @@ Those are the "components":

.. code-block:: python

physics_stepper = AdamsBashforth(
physics_stepper = AdamsBashforth([
UpdateFrequencyWrapper(Radiation(), timedelta(hours=2)),
BoundaryLayer(),
DeepConvection(),
)
])
implicit_dynamics = ImplicitDynamics()

:py:class:`~sympl.AdamsBashforth` is a :py:class:`~sympl.TimeStepper`, which is
Expand Down Expand Up @@ -163,12 +170,16 @@ computation is done -- the main loop:
while state['time'] < datetime(2010, 1, 1):
physics_diagnostics, state_after_physics = physics_stepper(state, timestep)
dynamics_diagnostics, next_state = implicit_dynamics(state_after_physics, timestep)
state_after_physics.update(physics_diagnostics)
state_after_physics.update(dynamics_diagnostics)
plot_monitor.store(state_after_physics)
state.update(physics_diagnostics)
state.update(dynamics_diagnostics)
plot_monitor.store(state)
next_state['time'] = state['time'] + timestep
state = next_state

In the main loop, a series of component calls update the state, and the figure
presented by ``plot_monitor`` is updated. The code is meant to be as
self-explanatory as possible.
self-explanatory as possible. It is necessary to manually set the time of the
next state at the end of the loop. This is not done automatically by
:py:class:`~sympl.TimeStepper` and :py:class:`~sympl.Implicit` objects, because
in many models you may want to update the state with multiple such objects
in a sequence over the course of a single time step.
20 changes: 13 additions & 7 deletions docs/state.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,21 @@ Choice of Datetime

The built-in ``datetime`` object in Python (as used above) assumes the
proleptic Gregorian calendar, which extends the Gregorian calendar back
infinitely. If you want to use a different type of calendar, you should use
a different datetime object, but one which has the same interface as the Python
object. To repeat, the calendar your model is using depends entirely on what
infinitely. Sympl provides a :py:func:`~sympl.datetime` function which returns
a datetime-like object, and allows a variety of different calendars. If a
calendar other than 'proleptic_gregorian' is specified, one of the classes from
the netcdftime_ package will be used. Of course, this requires that it is
installed! If it's not, you will get an error, and should ``pip install netcdftime``.
Sympl also includes :py:class:`~sympl.timedelta` for convenience. This is just
the default Python ``timedelta``.

To repeat, the calendar your model is using depends entirely on what
object you're using to store time in the state dictionary, and the default one
uses the proleptic Gregorian calendar.
uses the proleptic Gregorian calendar used by the default Python ``datetime``.

We think and hope the netcdftime_ package will have objects with this
functionality. Once it's available on pypi and documented, update this doc
page to describe how to use it! Or write your own package and let us know!
.. autofunction:: sympl.datetime

.. autoclass:: sympl.timedelta

.. _netcdftime: https://github.com/Unidata/netcdftime

Expand Down
18 changes: 16 additions & 2 deletions docs/timestepping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,24 @@ This means you will sometimes want to pass ``state`` to your
:py:class:`~sympl.Monitor` objects *after* calling
the :py:class:`~sympl.TimeStepper` and getting ``next_state``.

.. warning:: :py:class:`~sympl.TimeStepper` objects do not, and should not,
update 'time' in the model state.

Keep in mind that for split-time models, multiple :py:class:`~sympl.TimeStepper`
objects might be called in in a single pass of the main loop. There are also
objects might be called in in a single pass of the main loop. If each one
updated ``state['time']``, the time would be moved forward more than it should.
For that reason, :py:class:`~sympl.TimeStepper` objects do not update
``state['time']``.

There are also
:py:class:`~sympl.Implicit` objects which evolve the state forward in time
without the use of Prognostic objects.
without the use of Prognostic objects. These function exactly the same as a
:py:class:`~sympl.TimeStepper` once they are created, but do not accept
:py:class:`~sympl.Prognostic` objects when you create them. One example might
be a component that condenses all supersaturated moisture over some time period.
:py:class:`~sympl.Implicit` objects are generally used for parameterizations
that work by determining the target model state in some way, or involve
limiters, and cannot be represented as a :py:class:`~sympl.Prognostic`.

.. autoclass:: sympl.TimeStepper
:members:
Expand Down
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ scipy
numpy>=0.10
coveralls
netcdf4
cython
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.1
current_version = 0.3.0
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

setup(
name='sympl',
version='0.2.1',
version='0.3.0',
description='Sympl is a Toolkit for building Earth system models in Python.',
long_description=readme + '\n\n' + history,
author="Jeremy McGibbon",
Expand Down
8 changes: 5 additions & 3 deletions sympl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from ._core.base_components import (
Prognostic, Diagnostic, Implicit, Monitor, PrognosticComposite,
DiagnosticComposite, MonitorComposite
DiagnosticComposite, MonitorComposite, ImplicitPrognostic
)
from ._core.timestepping import TimeStepper, Leapfrog, AdamsBashforth
from ._core.exceptions import (
Expand All @@ -24,11 +24,12 @@
from ._components import (
PlotFunctionMonitor, NetCDFMonitor, RestartMonitor,
ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic)
from ._core.time import datetime, timedelta

__version__ = '0.2.1'
__version__ = '0.3.0'
__all__ = (
Prognostic, Diagnostic, Implicit, Monitor, PrognosticComposite,
DiagnosticComposite, MonitorComposite,
DiagnosticComposite, MonitorComposite, ImplicitPrognostic,
TimeStepper, Leapfrog, AdamsBashforth,
InvalidStateError, SharedKeyError, DependencyError,
InvalidPropertyDictError,
Expand All @@ -44,4 +45,5 @@
ComponentTestBase,
PlotFunctionMonitor, NetCDFMonitor, RestartMonitor,
ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic,
datetime, timedelta
)
66 changes: 66 additions & 0 deletions sympl/_core/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from datetime import datetime as real_datetime, timedelta
from .exceptions import DependencyError
try:
import netcdftime as nt
except ImportError:
nt = None


def datetime(
year, month, day, hour=0, minute=0, second=0, microsecond=0,
tzinfo=None, calendar='proleptic_gregorian'):
"""
Retrieves a datetime-like object with the requested calendar. Calendar types
other than proleptic_gregorian require the netcdftime module to be
installed.

Parameters
----------
year : int,
month : int,
day : int,
hour : int, optional
minute : int, optional
second : int, optional
microsecond : int, optional
tzinfo : datetime.tzinfo, optional
A timezone informaton class, such as from pytz. Can only be used with
'proleptic_gregorian' calendar, as netcdftime does not support
timezones.
calendar : string, optional
Should be one of 'proleptic_gregorian', 'no_leap', '365_day',
'all_leap', '366_day', '360_day', 'julian', or 'gregorian'. Default
is 'proleptic_gregorian', which returns a normal Python datetime.
Other options require the netcdftime module to be installed.

Returns
-------
datetime : datetime-like
The requested datetime. May be a Python datetime, or one of the
datetime-like types in netcdftime.
"""
kwargs = {
'year': year, 'month': month, 'day': day, 'hour': hour,
'minute': minute, 'second': second, 'microsecond': microsecond
}
if calendar.lower() == 'proleptic_gregorian':
return real_datetime(tzinfo=tzinfo, **kwargs)
elif tzinfo is not None:
raise ValueError('netcdftime does not support timezone-aware datetimes')
elif nt is None:
raise DependencyError(
"Calendars other than 'proleptic_gregorian' require the netcdftime "
"package, which is not installed.")
elif calendar.lower() in ('all_leap', '366_day'):
return nt.DatetimeAllLeap(**kwargs)
elif calendar.lower() in ('no_leap', 'noleap', '365_day'):
return nt.DatetimeNoLeap(**kwargs)
elif calendar.lower() == '360_day':
return nt.Datetime360Day(**kwargs)
elif calendar.lower() == 'julian':
return nt.DatetimeJulian(**kwargs)
elif calendar.lower() == 'gregorian':
return nt.DatetimeGregorian(**kwargs)


__all__ = (datetime, timedelta)