from __future__ import division
from __future__ import print_function
from builtins import str
from builtins import range
import numpy as np
import copy
from climlab import constants as const
from .process import Process
from climlab.utils import walk
from climlab.utils.attrdict import AttrDict
[docs]
def couple(proclist, name='Parent'):
# Union of the two state dictionaries
new_state = AttrDict()
new_input = AttrDict()
all_input = {}
all_diagnotics_list = []
timestep = const.seconds_per_year * 1E6 # very long!
for proc in proclist:
timestep = np.minimum(timestep, proc.timestep)
for key in proc.state:
new_state[key] = proc.state[key]
all_diagnotics_list += list(proc.diagnostics.keys())
for key in proc.input:
all_input[key] = proc.input[key]
for key in all_input:
if (key not in new_state) and (key not in all_diagnotics_list):
# This quantity is still a necessary input for the parent process
new_input[key] = all_input[key]
# The newly created parent process has the minimum timestep
coupled = TimeDependentProcess(state=new_state, timestep=timestep, name=name)
for proc in proclist:
coupled.add_subprocess(proc.name, proc)
for key in new_input:
coupled.add_input(key, new_input[key])
return coupled
[docs]
class TimeDependentProcess(Process):
"""A generic parent class for all time-dependent processes.
``TimeDependentProcess`` is a child of the
:class:`~climlab.process.process.Process` class and therefore inherits
all those attributes.
**Initialization parameters** \n
An instance of ``TimeDependentProcess`` is initialized with the following
arguments *(for detailed information see Object attributes below)*:
:param float timestep: specifies the timestep of the object (optional)
:param str time_type: how time-dependent-process should be computed
[default: 'explicit']
:param bool topdown: whether geneterate *process_types* in regular or
in reverse order [default: True]
**Object attributes** \n
Additional to the parent class :class:`~climlab.process.process.Process`
following object attributes are generated during initialization:
:ivar bool has_process_type_list:
information whether attribute *process_types*
(which is needed for :func:`compute` and build in
:func:`_build_process_type_list`)
exists or not. Attribute is set to ``'False'``
during initialization.
:ivar bool topdown: information whether the list *process_types* (which
contains all processes and sub-processes) should be
generated in regular or in reverse order.
See :func:`_build_process_type_list`.
:ivar dict timeave: a time averaged collection of all states and diagnostic
processes over the timeperiod that
:func:`integrate_years` has been called for last.
:ivar dict tendencies: computed difference in a timestep for each state.
See :func:`compute` for details.
:ivar str time_type: how time-dependent-process should be computed.
Possible values are: ``'explicit'``, ``'implicit'``,
``'diagnostic'``, ``'adjustment'``.
:ivar dict time: a collection of all time-related attributes of the process.
The dictionary contains following items:
* ``'timestep'``: see initialization parameter
* ``'num_steps_per_year'``: see :func:`set_timestep` and :func:`timestep` for details
* ``'day_of_year_index'``: counter how many steps have been integrated in current year
* ``'steps'``: counter how many steps have been integrated in total
* ``'days_elapsed'``: time counter for days
* ``'years_elapsed'``: time counter for years
* ``'days_of_year'``: array which holds the number of numerical steps per year, expressed in days
"""
def __init__(self, time_type='explicit', timestep=None, topdown=True, **kwargs):
# Create the state dataset
self.tendencies = {}
super(TimeDependentProcess, self).__init__(**kwargs)
for name, var in self.state.items():
self.tendencies[name] = var * 0.
self.timeave = {}
if timestep is None:
self.set_timestep()
else:
self.set_timestep(timestep=timestep)
self.time_type = time_type
self.topdown = topdown
self.has_process_type_list = False
def __add__(self, other):
newparent = couple([self,other])
return newparent
@property
def timestep(self):
"""The amount of time over which :func:`step_forward` is integrating in unit seconds.
:getter: Returns the object timestep which is stored in ``self.param['timestep']``.
:setter: Sets the timestep to the given input. See also :func:`set_timestep`.
:type: float
"""
return self.param['timestep']
@timestep.setter
def timestep(self, value):
num_steps_per_year = const.seconds_per_year / value
timestep_days = value / const.seconds_per_day
days_of_year = np.arange(0., const.days_per_year, timestep_days)
self.time = {'timestep': value,
'num_steps_per_year': num_steps_per_year,
'day_of_year_index': 0,
'steps': 0,
'days_elapsed': 0,
'years_elapsed': 0,
'days_of_year': days_of_year,
'active_now': True}
self.param['timestep'] = value
[docs]
def set_state(self, name, value):
super(TimeDependentProcess, self).set_state(name,value)
# Make sure that the new state variable is added to the tendencies dict
self.tendencies[name] = value * 0.
[docs]
def set_timestep(self, timestep=const.seconds_per_day, num_steps_per_year=None):
"""Calculates the timestep in unit seconds
and calls the setter function of :func:`timestep`
:param float timestep: the amount of time over which
:func:`step_forward` is integrating
in unit seconds [default: 24*60*60]
:param float num_steps_per_year: a number of steps per calendar year
(optional)
If the parameter *num_steps_per_year* is specified and not ``None``,
the timestep is calculated accordingly and therefore the given input
parameter *timestep* is ignored.
"""
if num_steps_per_year is not None:
timestep = const.seconds_per_year / num_steps_per_year
# Need a more sensible approach for annual cycle stuff
self.timestep = timestep
[docs]
def compute(self):
"""Computes the tendencies for all state variables given current state
and specified input.
The function first computes all diagnostic processes. They don't produce
any tendencies directly but they may affect the other processes (such as
change in solar distribution). Subsequently, all tendencies and
diagnostics for all explicit processes are computed.
Tendencies due to implicit and adjustment processes need to be
calculated from a state that is already adjusted after explicit
alteration. For that reason the explicit tendencies are applied to the
states temporarily. Now all tendencies from implicit processes are
calculated by matrix inversions and similar to the explicit tendencies,
the implicit ones are applied to the states temporarily. Subsequently,
all instantaneous adjustments are computed.
Then the changes that were made to the states from explicit and implicit
processes are removed again as this
:class:`~climlab.process.time_dependent_process.TimeDependentProcess.compute()`
function is supposed to calculate only tendencies and not apply them
to the states.
Finally, all calculated tendencies from all processes are collected
for each state, summed up and stored in the dictionary
``self.tendencies``, which is an attribute of the time-dependent-process
object, for which the
:class:`~climlab.process.time_dependent_process.TimeDependentProcess.compute()`
method has been called.
**Object attributes** \n
During method execution following object attributes are modified:
:ivar dict tendencies: dictionary that holds tendencies for all states
is calculated for current timestep through
adding up tendencies from explicit, implicit and
adjustment processes.
:ivar dict diagnostics: process diagnostic dictionary is updated
by diagnostic dictionaries of subprocesses
after computation of tendencies.
"""
# First reset tendencies to zero -- recomputing them is the point of this method
for varname in self.tendencies:
self.tendencies[varname] *= 0.
# Also reset all diagnostics to zero -- they will be accumulated from subprocesses
for diagname, diag in self.diagnostics.items():
diag *= 0.
if not self.has_process_type_list:
self._build_process_type_list()
tendencies = {}
ignored = self._compute_type('diagnostic')
tendencies['explicit'] = self._compute_type('explicit')
# Tendencies due to implicit and adjustment processes need to be
# calculated from a state that is already adjusted after explicit stuff
# So apply the tendencies temporarily and then remove them again
for name, var in self.state.items():
var += tendencies['explicit'][name] * self.timestep
# Now compute all implicit processes -- matrix inversions
tendencies['implicit'] = self._compute_type('implicit')
# Same deal ... temporarily apply tendencies from implicit step
for name, var in self.state.items():
var += tendencies['implicit'][name] * self.timestep
# Finally compute all instantaneous adjustments -- expressed as explicit forward step
tendencies['adjustment'] = self._compute_type('adjustment')
# Now remove the changes from the model state
for name, var in self.state.items():
var -= ( (tendencies['implicit'][name] + tendencies['explicit'][name]) *
self.timestep)
# Sum up all subprocess tendencies
for proctype in ['explicit', 'implicit', 'adjustment']:
for varname, tend in tendencies[proctype].items():
self.tendencies[varname] += tend
# Finally compute my own tendencies, if any
self_tend = self._compute()
# Adjustment processes _compute method returns absolute adjustment
# Needs to be converted to rate of change
if self.time_type == 'adjustment':
for varname, adj in self_tend.items():
self_tend[varname] /= self.timestep
for varname, tend in self_tend.items():
self.tendencies[varname] += tend
# Now accumulate additive diagnostics from subprocesses
for procname, proc in self.subprocess.items():
for diagname, value in proc.diagnostics.items():
if self.__dict__[diagname].shape == value.shape:
self.__dict__[diagname] += value
return self.tendencies
def _compute_type(self, proctype):
"""Computes tendencies due to all subprocesses of given type
``'proctype'``. Also pass all diagnostics up to parent process."""
tendencies = {}
for varname in self.state:
tendencies[varname] = 0. * self.state[varname]
for proc in self.process_types[proctype]:
# Asynchronous coupling
# if subprocess has longer timestep than parent
# We compute subprocess tendencies once
# and apply the same tendency at each substep
step_ratio = int(proc.timestep / self.timestep)
# Does the number of parent steps divide evenly by the ratio?
# If so, it's time to do a subprocess step.
if self.time['steps'] % step_ratio == 0:
proc.time['active_now'] = True
tenddict = proc.compute()
else:
# proc.tendencies is unchanged from last subprocess timestep if we didn't recompute it above
proc.time['active_now'] = False
tenddict = proc.tendencies
for name, tend in tenddict.items():
tendencies[name] += tend
# for diagname, value in proc.diagnostics.items():
# # additive diagnostics...
# self.__setattr__(diagname, value)
return tendencies
def _compute(self):
"""Where the tendencies are actually computed...
Needs to be implemented for each daughter class
Returns a dictionary with same keys as self.state"""
tendencies = {}
for name, value in self.state.items():
tendencies[name] = value * 0.
return tendencies
def _build_process_type_list(self):
"""Generates lists of processes organized by process type.
Following object attributes are generated or updated:
:ivar dict process_types: a dictionary with entries:
``'diagnostic'``, ``'explicit'``,
``'implicit'`` and ``'adjustment'`` which
point to a list of processes according to
the process types.
The ``process_types`` dictionary is created while walking
through the processes with :func:`~climlab.utils.walk.walk_processes`
CHANGING THIS TO REFER ONLY TO THE CURRENT LEVEL IN SUBPROCESS TREE
"""
self.process_types = {'diagnostic': [], 'explicit': [], 'implicit': [], 'adjustment': []}
#for name, proc, level in walk.walk_processes(self, topdown=self.topdown):
# self.process_types[proc.time_type].append(proc)
for name, proc in self.subprocess.items():
self.process_types[proc.time_type].append(proc)
self.has_process_type_list = True
[docs]
def step_forward(self):
"""Updates state variables with computed tendencies.
Calls the :func:`compute` method to get current tendencies for all
process states. Multiplied with the timestep and added up to the state
variables is updating all model states.
:Example:
::
>>> import climlab
>>> model = climlab.EBM()
>>> # checking time step counter
>>> model.time['steps']
0
>>> # stepping the model forward
>>> model.step_forward()
>>> # step counter increased
>>> model.time['steps']
1
"""
tenddict = self.compute()
# Total tendency is applied as an explicit forward timestep
# (already accounting properly for order of operations in compute() )
for varname, tend in tenddict.items():
self.state[varname] += tend * self.timestep
# Update all time counters for this and all subprocesses in the tree
# Also pass diagnostics up the process tree
for name, proc, level in walk.walk_processes(self, ignoreFlag=True):
if proc.time['active_now']:
proc._update_time()
[docs]
def compute_diagnostics(self, num_iter=3):
"""Compute all tendencies and diagnostics, but don't update model state.
By default it will call compute() 3 times to make sure all
subprocess coupling is accounted for. The number of iterations can
be changed with the input argument.
"""
for n in range(num_iter):
ignored = self.compute()
def _update_time(self):
"""Increments the timestep counter by one.
Furthermore ``self.time['days_elapsed']`` and
``self.time['num_steps_per_year']`` are updated.
The function is called by the time stepping methods.
"""
self.time['steps'] += 1
# time in days since beginning
self.time['days_elapsed'] += self.time['timestep'] / const.seconds_per_day
if self.time['day_of_year_index'] >= self.time['num_steps_per_year']-1:
self._do_new_calendar_year()
else:
self.time['day_of_year_index'] += 1
def _do_new_calendar_year(self):
"""This function is called once at the end of every calendar year.
It updates ``self.time['years_elapsed']`` and
``self.time['day_of_year_index']``
"""
self.time['day_of_year_index'] = 0 # back to Jan. 1
self.time['years_elapsed'] += 1
[docs]
def integrate_years(self, years=1.0, verbose=True):
"""Integrates the model by a given number of years.
:param float years: integration time for the model in years
[default: 1.0]
:param bool verbose: information whether model time details
should be printed [default: True]
It calls :func:`step_forward` repetitively and calculates a time
averaged value over the integrated period for every model state and all
diagnostics processes.
:Example:
::
>>> import climlab
>>> model = climlab.EBM()
>>> model.global_mean_temperature()
Field(11.997968598413685)
>>> model.integrate_years(2.)
Integrating for 180 steps, 730.4844 days, or 2.0 years.
Total elapsed time is 2.0 years.
>>> model.global_mean_temperature()
Field(13.531055349437258)
"""
days = years * const.days_per_year
numsteps = int(self.time['num_steps_per_year'] * years)
if verbose:
print("Integrating for " + str(numsteps) + " steps, "
+ str(days) + " days, or " + str(years) + " years.")
# begin time loop
for count in range(numsteps):
# Compute the timestep
self.step_forward()
if count == 0:
# on first step only...
# This implements a generic time-averaging feature
# using the list of model state variables
self.timeave = self.state.copy()
# add any new diagnostics to the timeave dictionary
self.timeave.update(self.diagnostics)
# reset all values to zero
for varname, value in self.timeave.items():
# moves on to the next varname if value is None
# this preserves NoneType diagnostics
if value is None:
continue
self.timeave[varname] = 0*value
# adding up all values for each timestep
for varname in list(self.timeave.keys()):
try:
self.timeave[varname] += self.state[varname]
except:
try:
self.timeave[varname] += self.diagnostics[varname]
except: pass
# calculating mean values through dividing the sum by number of steps
for varname, value in self.timeave.items():
if value is None:
continue
self.timeave[varname] /= numsteps
if verbose:
print("Total elapsed time is %s years."
% str(self.time['days_elapsed']/const.days_per_year))
[docs]
def integrate_days(self, days=1.0, verbose=True):
"""Integrates the model forward for a specified number of days.
It convertes the given number of days into years and calls
:func:`integrate_years`.
:param float days: integration time for the model in days
[default: 1.0]
:param bool verbose: information whether model time details
should be printed [default: True]
:Example:
::
>>> import climlab
>>> model = climlab.EBM()
>>> model.global_mean_temperature()
Field(11.997968598413685)
>>> model.integrate_days(80.)
Integrating for 19 steps, 80.0 days, or 0.219032740466 years.
Total elapsed time is 0.211111111111 years.
>>> model.global_mean_temperature()
Field(11.873680783355553)
"""
years = days / const.days_per_year
self.integrate_years(years=years, verbose=verbose)
[docs]
def integrate_converge(self, crit=1e-4, verbose=True):
"""Integrates the model until model states are converging.
:param crit: exit criteria for difference of iterated
solutions [default: 0.0001]
:type crit: float
:param bool verbose: information whether total elapsed time
should be printed [default: True]
:Example:
::
>>> import climlab
>>> model = climlab.EBM()
>>> model.global_mean_temperature()
Field(11.997968598413685)
>>> model.integrate_converge()
Total elapsed time is 10.0 years.
>>> model.global_mean_temperature()
Field(14.288155406577301)
"""
# implemented by m-kreuzer
for varname, value in self.state.items():
value_old = copy.deepcopy(value)
self.integrate_years(1,verbose=False)
while np.max(np.abs(value_old-value)) > crit :
value_old = copy.deepcopy(value)
self.integrate_years(1,verbose=False)
if verbose == True:
print("Total elapsed time is %s years."
% str(self.time['days_elapsed']/const.days_per_year))