From 9519b926bbcc7a0246bc6ee3046c01895758afc0 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Tue, 6 Mar 2018 19:40:16 +0100 Subject: [PATCH 01/97] implement variable and process using attr --- xsimlab/process.py | 286 ++++++++-------------- xsimlab/variable.py | 91 +++++++ xsimlab/variable/__init__.py | 0 xsimlab/variable/base.py | 460 ----------------------------------- xsimlab/variable/custom.py | 93 ------- 5 files changed, 195 insertions(+), 735 deletions(-) create mode 100644 xsimlab/variable.py delete mode 100644 xsimlab/variable/__init__.py delete mode 100644 xsimlab/variable/base.py delete mode 100644 xsimlab/variable/custom.py diff --git a/xsimlab/process.py b/xsimlab/process.py index b630fb52..b81015b1 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -1,237 +1,159 @@ -import sys -import inspect -import copy -from collections import OrderedDict - -from .variable.base import (AbstractVariable, DiagnosticVariable, - VariableList, VariableGroup) -from .formatting import process_info -from .utils import AttrMapping, combomethod +from inspect import isclass +import attr -_process_meta_default = { - 'time_dependent': True -} +from .variable import AttrType -def _extract_variables(mapping): - # type: Dict[str, Any] -> Tuple[ - # Dict[str, Union[AbstractVariable, Variablelist, VariableGroup]], - # Dict[str, Any]] +def get_variables(process, attr_type=None): + if not isclass(process): + process = process.__class__ - var_dict = {} + if attr_type is None: + return [field for field in attr.fields(process)] - for key, value in mapping.items(): - if isinstance(value, (AbstractVariable, VariableList, VariableGroup)): - var_dict[key] = value + else: + return [field for field in attr.fields(process) + if field.metadata['attr_type'] == AttrType(attr_type)] - elif getattr(value, '_diagnostic', False): - var = DiagnosticVariable(value, description=value.__doc__, - attrs=value._diagnostic_attrs) - var_dict[key] = var - no_var_dict = {k: v for k, v in mapping.items() if k not in var_dict} +def _attrify_class(cls): + """Return a class as if the input class `cls` was + decorated with `attr.s`. - return var_dict, no_var_dict + `attr.s` turns `attr.ib` (or derived) class attributes + into fields and adds dunder-methods such as `__init__`. + """ + def post_init(self): + """Init instance attributes that will be used + during model creation or simulation runtime. + """ + self.name = None + self.__xsimlab_store__ = None + self.__xsimlab_store_keys__ = {} -class ProcessBase(type): - """Metaclass for all processes.""" - - def __new__(cls, name, bases, attrs): - parents = [b for b in bases if isinstance(b, ProcessBase)] - if not parents: - # Skip customization for the `Process` class - # (only applied to its subclasses) - new_attrs = attrs.copy() - new_attrs.update({'_variables': {}, '_meta': {}}) - return super().__new__(cls, name, bases, new_attrs) - for p in parents: - mro = [c for c in inspect.getmro(p) - if isinstance(c, ProcessBase)] - if len(mro) > 1: - # Currently not supported to create a class that - # inherits from a subclass of Process - raise TypeError("subclassing a subclass of Process " - "is not supported") - - # start with new attributes - new_attrs = {'__module__': attrs.pop('__module__')} - classcell = attrs.pop('__classcell__', None) - if classcell is not None: - new_attrs['__classcell__'] = classcell # pragma: no cover - - # check and add metadata - meta_cls = attrs.pop('Meta', None) - meta_dict = _process_meta_default.copy() - - if meta_cls is not None: - meta_attrs = {k: v for k, v in meta_cls.__dict__.items() - if not k.startswith('__')} - invalid_attrs = set(meta_attrs) - set(meta_dict) - if invalid_attrs: - keys = ", ".join(["%r" % k for k in invalid_attrs]) - raise AttributeError( - "invalid attribute(s) %s set in class %s.Meta" - % (keys, cls.__name__) - ) - meta_dict.update(meta_attrs) + setattr(cls, '__attrs_post_init__', post_init) - new_attrs['_meta'] = OrderedDict(sorted(meta_dict.items())) + return attr.attrs(cls) - # add variables and diagnostics separately from the rest of - # the attributes and methods defined in the class - vars, novars = _extract_variables(attrs) - new_attrs['_variables'] = OrderedDict(sorted(vars.items())) - for k, v in novars.items(): - new_attrs[k] = v - new_class = super().__new__(cls, name, bases, new_attrs) +def _make_property_variable(var): + """Create a property for a variable.""" - return new_class + var_name = var.name + def getter(self): + return self.__xsimlab_store__[(self.name, var_name)] -class Process(AttrMapping, metaclass=ProcessBase): - """Base class that represents a logical unit in a computational model. + def setter(self, value): + self.__xsimlab_store__[(self.name, var_name)] = value - A subclass of `Process` usually implements: + return property(fget=getter, fset=setter) - - A process interface as a set of `Variable`, `ForeignVariable`, - `VariableGroup` or `VariableList` objects, all defined as class - attributes. - - Some of the five `.validate()`, `.initialize()`, `.run_step()`, - `.finalize_step()` and `.finalize()` methods, which use or compute - values of the variables defined in the interface during a model run. +def _make_property_derived(var): + """Create a read-only property for a derived variable.""" - - Additional methods decorated with `@diagnostic` that compute - the values of diagnostic variables during a model run. + var_name = var.name - Once created, a `Process` object provides both dict-like and - attribute-like access for all its variables, including diagnostic - variables if any. + if 'compute' not in var.metadata: + raise KeyError("no compute method found for derived variable '{name}': " + "a method decorated with '@{name}.compute' is required " + "in the class definition.".format(name=var.name)) - """ - def __init__(self): - # prevent modifying variables at the class level. also prevent - # using the same variable objects in two distinct instances - self._variables = copy.deepcopy(self._variables) + func_compute_value = var.metadata['compute'] - super(Process, self).__init__(self._variables) + def getter(self): + value = func_compute_value(self) + self.__xsimlab_store__[(self.name, var_name)] = value + return value - for var in self._variables.values(): - if isinstance(var, DiagnosticVariable): - var.assign_process_obj(self) + return property(fget=getter) - self._name = None - self._initialized = True - def clone(self): - """Clone the process. +def _make_property_foreign(var): + """Create a property for a foreign variable.""" - This is equivalent to a deep copy, except that variable data - (i.e., `state`, `value`, `change` or `rate` properties) are not copied. - """ - obj = type(self)() - obj._name = self._name - return obj + var_name = var.name - @property - def variables(self): - """A dictionary of Process variables.""" - return self._variables + def getter(self): + key = self.__xsimlab_store_keys__[var_name] + return self.__xsimlab_store__[key] - @property - def meta(self): - """A dictionary of Process metadata (i.e., Meta attributes).""" - return self._meta + def setter(self, value): + key = self.__xsimlab_store_keys__[var_name] + self.__xsimlab_store__[key] = value - @property - def name(self): - """Process name. + return property(fget=getter, fset=setter) - Returns the name of the Process subclass if it is not attached to - any Model object. - """ - if self._name is None: - return type(self).__name__ +def _make_property_group(var): + """Create a read-only property for a group variable.""" - return self._name + var_name = var.name - def validate(self): - """Validate and/or update the process variables values. + def getter(self): + for key in self.__xsimlab_store_keys__[var_name]: + yield self.__xsimlab_store__[key] - Implementation is optional (by default it does nothing). + return property(fget=getter) - An implementation of this method should be provided if the process - has variables that are optional and/or that depend on other - variables defined in this process. - To validate values of variables taken independently, it is - prefered to use Variable validators. +class _ProcessBuilder(object): + """Used to iteratively create a new process class. - See Also - -------- - Variable.validators + The original class must be already "attr-yfied", i.e., + it must correspond to a class returned by `attr.attrs`. - """ - pass # pragma: no cover - - def initialize(self): - """This method will be called once at the beginning of a model run. + """ + _make_prop_funcs = { + AttrType.VARIABLE: _make_property_variable, + AttrType.DERIVED: _make_property_derived, + AttrType.FOREIGN: _make_property_foreign, + AttrType.GROUP: _make_property_group + } - Implementation is optional (by default it does nothing). - """ - pass # pragma: no cover + def __init__(self, attr_cls): + self._cls = attr_cls + self._cls_dict = {} - def run_step(self, *args): - """This method will be called at every time step of a model run. + def add_properties(self, attr_type): + make_prop_func = self._make_prop_funcs[attr_type] - It should accepts one argument that corresponds to the time step - duration. + for var in get_variables(self._cls, attr_type): + self._cls_dict[var.name] = make_prop_func(var) - This must be implemented for all time dependent processes. - """ - raise NotImplementedError( - "class %s has no method 'run_step' implemented" - % type(self).__name__ - ) + def render_docstrings(self): + self._cls_dict['__doc__'] = "Process-ified class." - def finalize_step(self): - """This method will be called at the end of every time step, i.e, - after `run_step` has been executed for all processes in a model. + def build_class(self): + cls = self._cls - Implementation is optional (by default it does nothing). - """ - pass # pragma: no cover + # Attach properties (and docstrings) + for name, value in self._cls_dict.items(): + setattr(cls, name, value) - def finalize(self): - """This method will be called once at the end of a model run. + return cls - Implementation is optional (by default does nothing). - """ - pass # pragma: no cover - @combomethod - def info(cls_or_self, buf=None): - """info(buf=None) +def process(maybe_cls=None, autodoc=False): + """Decorator to define a class as a process.""" - Concise summary of Process variables and metadata. + def wrap(cls): + attr_cls = _attrify_class(cls) - Parameters - ---------- - buf : object, optional - Writable buffer (default: sys.stdout). + builder = _ProcessBuilder(attr_cls) - """ - if buf is None: # pragma: no cover - buf = sys.stdout + for attr_type in AttrType: + builder.add_properties(attr_type) - buf.write(process_info(cls_or_self)) + if autodoc: + builder.render_docstrings() - def __repr__(self): - cls = "'%s.%s'" % (self.__module__, type(self).__name__) - header = "\n" % cls + return builder.build_class() - return header + process_info(self) + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) diff --git a/xsimlab/variable.py b/xsimlab/variable.py new file mode 100644 index 00000000..5ada7f80 --- /dev/null +++ b/xsimlab/variable.py @@ -0,0 +1,91 @@ +# coding: utf-8 +from enum import Enum + +import attr +from attr._make import _CountingAttr as CountingAttr_ + + +class AttrType(Enum): + VARIABLE = 'variable' + DERIVED = 'derived' + FOREIGN = 'foreign' + GROUP = 'group' + + +class _CountingAttr(CountingAttr_): + """A hack to add a custom 'compute' decorator for on-request computation + of derived variables. + """ + + def compute(self, method): + self.metadata['compute'] = method + + return method + + +def variable(dims=(), intent='input', group=None, attrs=None, description=''): + metadata = {'attr_type': AttrType.VARIABLE, + 'dims': dims, + 'intent': intent, + 'group': group, + 'attrs': attrs, + 'description': description} + + return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) + + +def derived(dims=(), group=None, attrs=None, description=''): + """A derived variable that is computed on request. + + Instead of being computed systematically at every step of a simulation + or at initialization, its value is only computed (or re-computed) + each time when it is needed. + + Defined in a process class, such variable requires a method + to compute its value, which must be defined in the same class and + decorated (e.g., using `@myvar.compute` if the name of the variable + is `myvar`). + + A derived variable never accepts an input value (i.e., intent='output'), + and should never be set/updated. + + Its computation usually involves other variables (hence the + term `derived`), although this is not required. + + These variables may be useful, e.g., for model diagnostics. + + """ + metadata = {'attr_type': AttrType.DERIVED, + 'dims': dims, + 'intent': 'output', + 'group': group, + 'attrs': attrs, + 'description': description} + + return _CountingAttr( + default=attr.NOTHING, + validator=None, + repr=False, + cmp=False, + hash=None, + init=False, + converter=None, + metadata=metadata, + type=None, + ) + + +def foreign(other_process_cls, var_name, intent='input'): + metadata = {'attr_type': AttrType.FOREIGN, + 'other_process_cls': other_process_cls, + 'var_name': var_name, + 'intent': intent} + + return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) + + +def group(name): + metadata = {'attr_type': AttrType.GROUP, + 'group': group} + + return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) diff --git a/xsimlab/variable/__init__.py b/xsimlab/variable/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/xsimlab/variable/base.py b/xsimlab/variable/base.py deleted file mode 100644 index 3ae11529..00000000 --- a/xsimlab/variable/base.py +++ /dev/null @@ -1,460 +0,0 @@ -# coding: utf-8 -""" -Base classes for Variable objects. - -""" -import itertools -from collections import Iterable, OrderedDict - -from xarray.core.variable import as_variable - - -class ValidationError(ValueError): - """An error while validating data.""" - pass - - -class AbstractVariable(object): - """Abstract class for all variables. - - This class aims at providing a common parent class - for all regular, diagnostic, foreign and undefined variables. - - """ - def __init__(self, provided=False, description='', attrs=None, - group=None): - self.provided = provided - self.description = description - self.attrs = attrs or {} - self.group = group - - def __repr__(self): - return "" % type(self).__name__ - - -class Variable(AbstractVariable): - """Base class that represents a variable in a process or a model. - - `Variable` objects store useful metadata such as dimension labels, - a short description, a default value or other user-provided metadata. - - Variables allow to convert any given value to a `xarray.Variable` object - after having perfomed some checks. - - In processes, variables are instantiated as class attributes. They - represent fundamental elements of a process interface (see - :class:`Process`) and by extension a model interface. - Some attributes such as `provided` and `optional` also contribute to - the definition of the interface. - - """ - default_validators = [] # Default set of validators - - def __init__(self, allowed_dims, provided=False, optional=False, - default_value=None, validators=(), group=None, - description='', attrs=None): - """ - Parameters - ---------- - allowed_dims : str or tuple or list - Dimension label(s) allowed for the variable. An empty tuple - corresponds to a scalar variable, a string or a 1-length tuple - corresponds to a 1-d variable and a n-length tuple corresponds to a - n-d variable. A list of str or tuple items may also be provided if - the variable accepts different numbers of dimensions. - This should not include a time dimension, which is always allowed. - provided : bool, optional - Defines whether a value for the variable is required (False) - or provided (True) by the process in which it is defined - (default: False). - If `provided=True`, then the variable in a process/model won't - be considered as an input of that process/model. - optional : bool, optional - If True, a value may not be required for the variable - (default: False). Ignored when `provided` is True. - default_value : object, optional - Single default value for the variable (default: None). It - will be automatically broadcasted to all of its dimensions. - Ignored when `provided` is True. - validators : tuple or list, optional - A list of callables that take an `xarray.Variable` object and - raises a `ValidationError` if it doesn’t meet some criteria. - It may be useful for custom, advanced validation that - can be reused for different variables. - group : str, optional - Variable group. - description : str, optional - Short description of the variable (ideally one-line). - attrs : dict, optional - Dictionnary of additional metadata (e.g., standard_name, - units, math_symbol...). - - """ - super(Variable, self).__init__(provided=provided, group=group, - description=description, - attrs=attrs) - - if not len(allowed_dims): - allowed_dims = [()] - elif isinstance(allowed_dims, str): - allowed_dims = [(allowed_dims,)] - elif isinstance(allowed_dims, list): - allowed_dims = [tuple([d]) if isinstance(d, str) else tuple(d) - for d in allowed_dims] - else: - allowed_dims = [allowed_dims] - self.allowed_dims = tuple(allowed_dims) - - self.optional = optional - self.default_value = default_value - self._validators = list(validators) - self._state = None - self._rate = None - self._change = None - - @property - def validators(self): - return list(itertools.chain(self.default_validators, self._validators)) - - def run_validators(self, xr_variable): - for vfunc in self.validators: - vfunc(xr_variable) - - def validate(self, xr_variable): - pass # pragma: no cover - - def validate_dimensions(self, dims, ignore_dims=None): - """Validate given dimensions for this variable. - - Parameters - ---------- - dims : tuple - Dimensions given for the variable. - ignore_dims : tuple, optional - Dimensions that are ignored during the validation process. - Typically, it may correspond to a time dimension (time steps) - and/or a grid-search or parameter-space dimension for this - variable itself. - - Raises - ------ - ValidationError - If the given dimensions are not valid, i.e., if they doesn't - correspond to allowed dimensions for this variable. - - """ - dims_dict = OrderedDict([(d, None) for d in dims]) - - if ignore_dims is not None: - for d in ignore_dims: - dims_dict.pop(d, None) - - clean_dims = tuple(dims_dict) - test_dims = [d for d in self.allowed_dims if d == clean_dims] - - if not test_dims: - raise ValidationError("invalid dimensions %s\n" - "allowed dimensions are %s" - % (dims, self.allowed_dims)) - - def to_xarray_variable(self, value): - """Convert the input value to an `xarray.Variable` object. - - Parameters - ---------- - value : object - The input value can be in the form of a single value, - an array-like, a ``(dims, data[, attrs])`` tuple, another - `xarray.Variable` object or a `xarray.DataArray` object. - if None, the `default_value` attribute is used to set the value. - - Returns - ------- - variable : `xarray.Variable` - A xarray Variable object whith data corresponding to the input (or - default) value and with attributes ('description' and other - key:value pairs found in `Variable.attrs`). - - """ - if value is None: - value = self.default_value - - # in case where value is a 1-d array without dimension name, - # dimension name is set to 'this_variable' and has to be renamed - # later by the name of the variable in a process/model. - xr_variable = as_variable(value, name='this_variable') - - xr_variable.attrs.update(self.attrs) - if self.description: - xr_variable.attrs['description'] = self.description - - return xr_variable - - @property - def state(self): - """State value of the variable, i.e., the instant value at a given - time or simply the value if the variable is not time dependent. - """ - return self._state - - @state.setter - def state(self, value): - self._state = value - - value = state - - @property - def rate(self): - """Rate value of the variable, i.e., the rate of change in time - (time derivative). - """ - return self._rate - - @rate.setter - def rate(self, value): - self._rate = value - - @property - def change(self): - """Change value of the variable, i.e., the rate of change in time - (time derivative) integrated over the time step. - """ - return self._change - - @change.setter - def change(self, value): - self._change = value - - def __repr__(self): - dims_str = ', '.join(['(%s)' % ', '.join(['%r' % d for d in dims]) - for dims in self.allowed_dims]) - return ("" % (type(self).__name__, dims_str)) - - -class ForeignVariable(AbstractVariable): - """Reference to a variable that is defined in another `Process` class. - - """ - def __init__(self, other_process, var_name, provided=False): - """ - Parameters - ---------- - other_process : str or class - Class or class name in which the variable is defined. - var_name : str - Name of the corresponding class attribute in `other_process`. - The value of this class attribute must be a `Variable` object. - provided : bool, optional - Defines whether a value for the variable is required (False) or - provided (True) by the process in which this reference is - defined (default: False). - - """ - super(ForeignVariable, self).__init__(provided=provided) - - self._other_process_cls = other_process - self._other_process_obj = None - self.var_name = var_name - - @property - def ref_process(self): - """The process where the original variable is defined. - - Returns either the Process class or a Process instance attached to - a model. - """ - if self._other_process_obj is None: - return self._other_process_cls - - return self._other_process_obj - - @property - def ref_var(self): - """Returns the original Variable object.""" - return self.ref_process.variables[self.var_name] - - @property - def state(self): - """State value of the original Variable object.""" - return self.ref_var._state - - @state.setter - def state(self, value): - self.ref_var.state = value - - value = state - - @property - def rate(self): - """Rate value of the original Variable object.""" - return self.ref_var._rate - - @rate.setter - def rate(self, value): - self.ref_var.rate = value - - @property - def change(self): - """Change value of the original Variable object.""" - return self.ref_var._change - - @change.setter - def change(self, value): - self.ref_var.change = value - - def __repr__(self): - if self._other_process_obj is None: - ref_process_name = self._other_process_cls.__name__ - else: - ref_process_name = self.ref_process.name - - ref_str = "%s.%s" % (ref_process_name, self.var_name) - - return "" % (type(self).__name__, ref_str) - - -class DiagnosticVariable(AbstractVariable): - """Variable for model diagnostic purpose only. - - The value of a diagnostic variable is computed on the fly during a - model run (there is no initialization nor update of any state). - - A diagnostic variable is defined inside a `Process` subclass, but - it shouldn't be created directly as a class attribute. - Instead it should be defined by applying the `@diagnostic` decorator - on a method of that class. - - Diagnostic variables declared in a process should never be referenced - in other processes as foreign variable. - - The diagnostic variables declared in a process are computed after the - execution of all processes in a model at the end of a time step. - - """ - def __init__(self, func, description='', attrs=None): - super(DiagnosticVariable, self).__init__( - provided=True, description=description, attrs=attrs - ) - - self._func = func - self._process_obj = None - - def assign_process_obj(self, process_obj): - self._process_obj = process_obj - - @property - def state(self): - """State value of this variable (read-only), i.e., the instant value - at a given time or simply the value if the variable is time - independent. - """ - return self._func(self._process_obj) - - value = state - - def __call__(self): - return self.state - - -def diagnostic(attrs_or_function=None, attrs=None): - """Applied to a method of a `Process` subclass, this decorator - allows registering that method as a diagnostic variable. - - The method's docstring is used as a description of the - variable (it should be short, one-line). - - Parameters - ---------- - attrs : dict (optional) - Variable metadata (e.g., standard_name, units, math_symbol...). - - Examples - -------- - .. code-block:: python - - @diagnostic - def slope(self): - '''topographic slope''' - return self._compute_slope() - - @diagnostic({'units': '1/m'}) - def curvature(self): - '''terrain curvature''' - return self._compute_curvature() - - """ - func = None - if callable(attrs_or_function): - func = attrs_or_function - elif isinstance(attrs_or_function, dict): - attrs = attrs_or_function - - def _add_diagnostic_attrs(function): - function._diagnostic = True - function._diagnostic_attrs = attrs - return function - - if func is not None: - return _add_diagnostic_attrs(func) - else: - return _add_diagnostic_attrs - - -class VariableList(tuple): - """A tuple of only `Variable` or `ForeignVariable` objects.""" - def __new__(cls, variables): - var_list = [var for var in variables - if isinstance(var, (Variable, ForeignVariable))] - - if len(var_list) != len(variables): - raise ValueError("found variables mixed with objects of other " - "types in %s" % variables) - - return tuple.__new__(cls, var_list) - - -class VariableGroup(Iterable): - """An Iterable of `ForeignVariable` objects for all variables in a Model - that belong to the same group. - - Using `VariableGroup` is useful in cases when we want to reuse - the same process in different contexts, i.e., with processes that may be - different from one model to another. Good examples are processes that - aggregate (e.g., sum, product, mean) variables defined in other processes. - - """ - def __init__(self, group): - """ - Parameters - ---------- - group : str - Name of the group. - - """ - self._variables = None - - # TODO: inherit also from AbstractVariable? - self.group = group - self.provided = False - self.description = '' - self.attrs = {} - - def _set_variables(self, processes): - """Retrieve all variables in `processes` that belong to this group.""" - self._variables = [] - for proc in processes.values(): - for var_name, var in proc._variables.items(): - if isinstance(var, VariableGroup): - continue - if var.group is not None and var.group == self.group: - foreign_var = ForeignVariable(proc.__class__, var_name) - self._variables.append(foreign_var) - - def __iter__(self): - if self._variables is None: - raise ValueError("cannot retrieve variables of group %r, " - "no model assigned yet." % self.group) - return (v for v in self._variables) - - def __repr__(self): - return "" % (type(self).__name__, self.group) diff --git a/xsimlab/variable/custom.py b/xsimlab/variable/custom.py deleted file mode 100644 index f815b828..00000000 --- a/xsimlab/variable/custom.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Custom Variable sub-classes. - -""" - -from functools import partial - -import numpy as np - -from .base import Variable, ValidationError - - -def dtype_validator(variable, expected_dtypes): - if not isinstance(expected_dtypes, (list, tuple)): - expected_dtypes = [expected_dtypes] - - test_dtype = any([np.issubdtype(variable.dtype, dtype) - for dtype in expected_dtypes]) - - if not test_dtype: - raise ValidationError( - "invalid dtype, expected one between %s, found %r)" - % ([np.dtype(dtype) for dtype in expected_dtypes], variable.dtype)) - - -floating_validator = partial(dtype_validator, - expected_dtypes=[np.floating, np.integer]) -integer_validator = partial(dtype_validator, expected_dtypes=np.integer) - - -class NumberVariable(Variable): - """A variable that accept numbers as values.""" - - def __init__(self, allowed_dims, bounds=(None, None), - inclusive_bounds=(True, True), **kwargs): - """ - Parameters - ---------- - allowed_dims : str or tuple or list - Dimension label(s) allowed for the variable. An empty tuple - corresponds to a scalar variable, a string or a 1-length tuple - corresponds to a 1-d variable and a n-length tuple corresponds to a - n-d variable. A list of str or tuple items may also be provided if - the variable accepts different numbers of dimensions. - This should not include a time dimension, which is always allowed. - bounds : tuple or None, optional - (lower, upper) value bounds (default: no bounds). - inclusive_bounds : tuple, optional - Whether the given (lower, upper) bounds are inclusive or not. - Default: (True, True). - **kwargs - Keyword arguments of Variable. - - See Also - -------- - Variable - - """ - super(NumberVariable, self).__init__(allowed_dims, **kwargs) - self.bounds = bounds - self.inclusive_bounds = inclusive_bounds - - def _check_bounds(self, xr_variable): - vmin, vmax = self.bounds - incmin, incmax = self.inclusive_bounds - - tmin = ((vmin is not None) - and (xr_variable < vmin if incmin else xr_variable <= vmin)) - tmax = ((vmax is not None) - and (xr_variable > vmax if incmax else xr_variable >= vmax)) - - if np.any(tmin) or np.any(tmax): - smin = '[' if incmin else ']' - smax = ']' if incmax else '[' - strbounds = '{}{}, {}{}'.format(smin, vmin, vmax, smax) - raise ValidationError("found value(s) out of bounds %s" - % strbounds) - - def validate(self, xr_variable): - self._check_bounds(xr_variable) - super(NumberVariable, self).validate(xr_variable) # pragma: no cover - - -class FloatVariable(NumberVariable): - """A variable that accepts floating point numbers as values.""" - - default_validators = [floating_validator] - - -class IntegerVariable(NumberVariable): - """A variable that accepts integer numbers as values.""" - - default_validators = [integer_validator] From 2598f0c0e397354a26b052d475b98e1895804158 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 7 Mar 2018 18:03:38 +0100 Subject: [PATCH 02/97] add docstrings for variable factory functions + rename derived --- xsimlab/__init__.py | 6 +- xsimlab/process.py | 57 ++++++++++------- xsimlab/variable.py | 145 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 167 insertions(+), 41 deletions(-) diff --git a/xsimlab/__init__.py b/xsimlab/__init__.py index e04283cc..70beaf18 100644 --- a/xsimlab/__init__.py +++ b/xsimlab/__init__.py @@ -3,10 +3,8 @@ """ from .xr_accessor import SimlabAccessor, create_setup -from .variable.base import (Variable, ForeignVariable, VariableList, - VariableGroup, diagnostic, ValidationError) -from .variable.custom import NumberVariable, FloatVariable, IntegerVariable -from .process import Process +from .variable import variable, on_demand, foreign, group +from .process import get_variables, process from .model import Model from ._version import get_versions diff --git a/xsimlab/process.py b/xsimlab/process.py index b81015b1..8eae665d 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -6,6 +6,24 @@ def get_variables(process, attr_type=None): + """Helper function that returns variables declared in a process. + + Useful when one wants to access the variables metadata. + + Parameters + ---------- + process : object or class + Process class or object. + attr_type : {'variable', 'on_demand', 'foreign', 'group'}, optional + Only return variables of a given kind (by default, return all + variables). + + Returns + ------- + attributes : list + A list of :class:`attr.Attribute` objects. + + """ if not isclass(process): process = process.__class__ @@ -18,16 +36,17 @@ def get_variables(process, attr_type=None): def _attrify_class(cls): - """Return a class as if the input class `cls` was - decorated with `attr.s`. + """Return a class as if the input class `cls` was decorated with + `attr.s`. - `attr.s` turns `attr.ib` (or derived) class attributes - into fields and adds dunder-methods such as `__init__`. + `attr.s` turns `attr.ib` (or derived) class attributes into fields + and adds dunder-methods such as `__init__`. """ def post_init(self): - """Init instance attributes that will be used - during model creation or simulation runtime. + """Init instance attributes that will be used during model creation or + simulation runtime. + """ self.name = None self.__xsimlab_store__ = None @@ -52,22 +71,16 @@ def setter(self, value): return property(fget=getter, fset=setter) -def _make_property_derived(var): - """Create a read-only property for a derived variable.""" - - var_name = var.name +def _make_property_on_demand(var): + """Create a read-only property for an on-demand variable.""" if 'compute' not in var.metadata: - raise KeyError("no compute method found for derived variable '{name}': " - "a method decorated with '@{name}.compute' is required " - "in the class definition.".format(name=var.name)) - - func_compute_value = var.metadata['compute'] + raise KeyError("no compute method found for on_demand variable " + "'{name}': a method decorated with '@{name}.compute' " + "is required in the class definition." + .format(name=var.name)) - def getter(self): - value = func_compute_value(self) - self.__xsimlab_store__[(self.name, var_name)] = value - return value + getter = var.metadata['compute'] return property(fget=getter) @@ -103,13 +116,13 @@ def getter(self): class _ProcessBuilder(object): """Used to iteratively create a new process class. - The original class must be already "attr-yfied", i.e., - it must correspond to a class returned by `attr.attrs`. + The original class must be already "attr-yfied", i.e., it must + correspond to a class returned by `attr.attrs`. """ _make_prop_funcs = { AttrType.VARIABLE: _make_property_variable, - AttrType.DERIVED: _make_property_derived, + AttrType.ON_DEMAND: _make_property_on_demand, AttrType.FOREIGN: _make_property_foreign, AttrType.GROUP: _make_property_group } diff --git a/xsimlab/variable.py b/xsimlab/variable.py index 5ada7f80..bf1d8146 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -7,14 +7,14 @@ class AttrType(Enum): VARIABLE = 'variable' - DERIVED = 'derived' + ON_DEMAND = 'on_demand' FOREIGN = 'foreign' GROUP = 'group' class _CountingAttr(CountingAttr_): """A hack to add a custom 'compute' decorator for on-request computation - of derived variables. + of on_demand variables. """ def compute(self, method): @@ -23,7 +23,61 @@ def compute(self, method): return method -def variable(dims=(), intent='input', group=None, attrs=None, description=''): +def variable(dims=(), intent='input', group=None, default=attr.NOTHING, + validator=None, description='', attrs=None): + """Create a variable. + + Variables store useful metadata such as dimension labels, a short + description, a default value, validators or custom, + user-provided metadata. + + Variables are the primitives of the modeling framework, they + define the interface of each process in a model. + + Variables should be declared exclusively as class attributes in + process classes (i.e., classes decorated with `:func:process`). + + Parameters + ---------- + dims : str or tuple or list, optional + Dimension label(s) of the variable. An empty tuple + corresponds to a scalar variable (default), a string or a 1-length + tuple corresponds to a 1-d variable and a n-length tuple corresponds to + a n-d variable. A list of str or tuple items may also be provided if + the variable accepts different numbers of dimensions. + This should not include a time dimension, which may always be added. + intent : {'input', 'output'}, optional + Defines whether the variable is an input (i.e., a value is needed for + the process computation) or an output (i.e., the process provides a + value for that variable). + (default: 'input'). + group : str, optional + Variable group. + default : any, optional + Single default value for the variable (default: None). It + will be automatically broadcasted to all of its dimensions. + Ignored when ``intent='output'``. + validator : callable or list of callable, optional + Function that is called at simulation initialization (and possibly at + other times too) to check the value given for the variable. + The function must accept three arguments: + + - the process instance (access other variables) + - the variable object (access metadata) + - a passed value (check input). + + The function is expected to throw an exception in case of invalid + value. + If a ``list`` is passed, its items are treated as validators and must + all pass. + The validator can also be set using decorator notation. + description : str, optional + Short description of the variable. + attrs : dict, optional + Dictionnary of additional metadata (e.g., standard_name, + units, math_symbol...). + + """ metadata = {'attr_type': AttrType.VARIABLE, 'dims': dims, 'intent': intent, @@ -34,28 +88,49 @@ def variable(dims=(), intent='input', group=None, attrs=None, description=''): return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) -def derived(dims=(), group=None, attrs=None, description=''): - """A derived variable that is computed on request. +def on_demand(dims=(), group=None, description='', attrs=None): + """Create a variable that is computed on demand. Instead of being computed systematically at every step of a simulation or at initialization, its value is only computed (or re-computed) each time when it is needed. - Defined in a process class, such variable requires a method - to compute its value, which must be defined in the same class and - decorated (e.g., using `@myvar.compute` if the name of the variable - is `myvar`). + Like other variables, such variable should be declared in a + process class. Additionally, it requires its own method to compute + its value, which must be defined in the same class and decorated + (e.g., using `@myvar.compute` if the name of the variable is + `myvar`). - A derived variable never accepts an input value (i.e., intent='output'), - and should never be set/updated. + An on-demand variable never accepts an input value + (i.e., intent='output'), and should never be set/updated (read-only). - Its computation usually involves other variables (hence the - term `derived`), although this is not required. + Its computation usually involves other variables, although this is + not required. These variables may be useful, e.g., for model diagnostics. + Parameters + ---------- + dims : str or tuple or list, optional + Dimension label(s) of the variable. An empty tuple + corresponds to a scalar variable (default), a string or a 1-length + tuple corresponds to a 1-d variable and a n-length tuple corresponds to + a n-d variable. A list of str or tuple items may also be provided if + the variable accepts different numbers of dimensions. + This should not include a time dimension, which may always be added. + group : str, optional + description : str, optional + Short description of the variable. + attrs : dict, optional + Dictionnary of additional metadata (e.g., standard_name, + units, math_symbol...). + + See Also + -------- + :func:`variable` + """ - metadata = {'attr_type': AttrType.DERIVED, + metadata = {'attr_type': AttrType.ON_DEMAND, 'dims': dims, 'intent': 'output', 'group': group, @@ -76,6 +151,26 @@ def derived(dims=(), group=None, attrs=None, description=''): def foreign(other_process_cls, var_name, intent='input'): + """Create a reference to a variable that is defined in another + process class. + + Parameters + ---------- + other_process_cls : class + Class in which the variable is defined. + var_name : str + Name of the corresponding variable declared in `other_process_cls`. + intent : {'input', 'output'}, optional + Defines whether the variable is an input (i.e., a value is needed for + the process computation) or an output (i.e., the process provides a + value for that variable). + (default: 'input'). + + See Also + -------- + :func:`variable` + + """ metadata = {'attr_type': AttrType.FOREIGN, 'other_process_cls': other_process_cls, 'var_name': var_name, @@ -85,7 +180,27 @@ def foreign(other_process_cls, var_name, intent='input'): def group(name): + """Create a special variable which value returns an iterable of values of + variables in a model that all belong to the same group. + + Access to these variable values is read-only (i.e., intent='input'). + + Good examples of using group variables are processes that + aggregate (e.g., sum, product, mean) the values of variables that + are defined in various other processes in a model. + + Parameters + ---------- + group : str + Name of the group. + + See Also + -------- + :func:`variable` + + """ metadata = {'attr_type': AttrType.GROUP, - 'group': group} + 'group': group, + 'intent': 'input'} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From fc4ff6db486f19085edc655da9a1b86c74e3b0e9 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 8 Mar 2018 19:01:32 +0100 Subject: [PATCH 03/97] add checkers for variable arguments --- xsimlab/variable.py | 64 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index bf1d8146..2f657258 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -1,5 +1,5 @@ -# coding: utf-8 from enum import Enum +import itertools import attr from attr._make import _CountingAttr as CountingAttr_ @@ -23,6 +23,48 @@ def compute(self, method): return method +def _as_dim_tuple(dims): + """Return a tuple from one or more combination(s) of dimension labels + given in `dims` either as a tuple, str or list. + + Also ensure that the number of dimensions for each item in the + sequence is unique, e.g., dims=[('x', 'y'), ('y', 'x')] is + ambiguous and thus not allowed. + + """ + if not len(dims): + dims = [()] + elif isinstance(dims, str): + dims = [(dims,)] + elif isinstance(dims, list): + dims = [tuple([d]) if isinstance(d, str) else tuple(d) + for d in dims] + else: + dims = [dims] + + ndim_groups = [list(g) + for _, g in itertools.groupby(dims, lambda d: len(d))] + + if len(ndim_groups) != len(dims): + invalid_dims = [g for g in ndim_groups if len(g) > 1] + invalid_msg = ' and '.join( + ', '.join(str(d) for d in group) for group in invalid_dims + ) + + raise ValueError("the following combinations of dimension labels " + "are ambiguous for a variable: {}" + .format(invalid_msg)) + + return tuple(dims) + + +def _check_intent(intent): + if intent not in ('input', 'output'): + raise ValueError("invalid intent given for variable: must be " + "either 'input' or 'output', found '{}'" + .format(intent)) + + def variable(dims=(), intent='input', group=None, default=attr.NOTHING, validator=None, description='', attrs=None): """Create a variable. @@ -54,9 +96,8 @@ def variable(dims=(), intent='input', group=None, default=attr.NOTHING, group : str, optional Variable group. default : any, optional - Single default value for the variable (default: None). It - will be automatically broadcasted to all of its dimensions. - Ignored when ``intent='output'``. + Single default value for the variable, ignored when ``intent='output'`` + (default: None). A default value may also be set using a decorator. validator : callable or list of callable, optional Function that is called at simulation initialization (and possibly at other times too) to check the value given for the variable. @@ -79,13 +120,14 @@ def variable(dims=(), intent='input', group=None, default=attr.NOTHING, """ metadata = {'attr_type': AttrType.VARIABLE, - 'dims': dims, - 'intent': intent, + 'dims': _as_dim_tuple(dims), + 'intent': _check_intent(intent), 'group': group, - 'attrs': attrs, + 'attrs': attrs or {}, 'description': description} - return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) + return attr.attrib(metadata=metadata, default=default, validator=validator, + init=False, cmp=False, repr=False) def on_demand(dims=(), group=None, description='', attrs=None): @@ -131,10 +173,10 @@ def on_demand(dims=(), group=None, description='', attrs=None): """ metadata = {'attr_type': AttrType.ON_DEMAND, - 'dims': dims, + 'dims': _as_dim_tuple(dims), 'intent': 'output', 'group': group, - 'attrs': attrs, + 'attrs': attrs or {}, 'description': description} return _CountingAttr( @@ -174,7 +216,7 @@ def foreign(other_process_cls, var_name, intent='input'): metadata = {'attr_type': AttrType.FOREIGN, 'other_process_cls': other_process_cls, 'var_name': var_name, - 'intent': intent} + 'intent': _check_intent(intent)} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From 775ed3c655c7cd1698289c0d4a01197fdb0e03d0 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 8 Mar 2018 19:04:13 +0100 Subject: [PATCH 04/97] no fastpath access (store) for on-demand and read-only variables --- xsimlab/process.py | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 8eae665d..b54d6263 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -43,16 +43,17 @@ def _attrify_class(cls): and adds dunder-methods such as `__init__`. """ - def post_init(self): - """Init instance attributes that will be used during model creation or - simulation runtime. + def add_obj_attrs(self): + """Add instance attributes that are needed later during model creation + or simulation runtime. """ - self.name = None + self.__xsimlab_name__ = None + self.__xsimlab_model__ = None self.__xsimlab_store__ = None - self.__xsimlab_store_keys__ = {} + self.__xsimlab_foreign__ = {} - setattr(cls, '__attrs_post_init__', post_init) + setattr(cls, '__attrs_post_init__', add_obj_attrs) return attr.attrs(cls) @@ -63,10 +64,10 @@ def _make_property_variable(var): var_name = var.name def getter(self): - return self.__xsimlab_store__[(self.name, var_name)] + return self.__xsimlab_store__[(self.__xsimlab_name__, var_name)] def setter(self, value): - self.__xsimlab_store__[(self.name, var_name)] = value + self.__xsimlab_store__[(self.__xsimlab_name__, var_name)] = value return property(fget=getter, fset=setter) @@ -91,14 +92,24 @@ def _make_property_foreign(var): var_name = var.name def getter(self): - key = self.__xsimlab_store_keys__[var_name] - return self.__xsimlab_store__[key] + o_proc_name, o_var_name = self.__xsimlab_foreign__[var_name] + try: + return self.__xsimlab_store__[(o_proc_name, o_var_name)] + except KeyError: + # values of on_demand variables are not in the store + model = self.__xsimlab_model__ + return getattr(model._processes[o_proc_name], o_var_name) def setter(self, value): - key = self.__xsimlab_store_keys__[var_name] - self.__xsimlab_store__[key] = value + # no fastpath access (prevent setting read-only variables in store) + o_proc_name, o_var_name = self.__xsimlab_foreign__[var_name] + model = self.__xsimlab_model__ + return setattr(model._processes[o_proc_name], o_var_name, value) - return property(fget=getter, fset=setter) + if var.metadata['intent'] == 'input': + return property(fget=getter) + else: + return property(fget=getter, fset=setter) def _make_property_group(var): @@ -107,8 +118,13 @@ def _make_property_group(var): var_name = var.name def getter(self): - for key in self.__xsimlab_store_keys__[var_name]: - yield self.__xsimlab_store__[key] + for o_proc_name, o_var_name in self.__xsimlab_foreign__[var_name]: + try: + yield self.__xsimlab_store__[(o_proc_name, o_var_name)] + except KeyError: + # values of on_demand variables are not in the store + model = self.__xsimlab_model__ + return getattr(model._processes[o_proc_name], o_var_name) return property(fget=getter) From 5456420b6f3176a074a954e5a5c78471cff23346 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 8 Mar 2018 19:05:32 +0100 Subject: [PATCH 05/97] avoid error when attr.ib are added to process class --- xsimlab/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index b54d6263..d02217d6 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -32,7 +32,7 @@ def get_variables(process, attr_type=None): else: return [field for field in attr.fields(process) - if field.metadata['attr_type'] == AttrType(attr_type)] + if field.metadata.get('attr_type') == AttrType(attr_type)] def _attrify_class(cls): From 4f5dae3262f0d83727d9453a627e39b568a676f3 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 8 Mar 2018 19:06:52 +0100 Subject: [PATCH 06/97] add process docstrings --- xsimlab/process.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index d02217d6..c7c6d7f4 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -167,8 +167,37 @@ def build_class(self): def process(maybe_cls=None, autodoc=False): - """Decorator to define a class as a process.""" + """A class decorator that adds everything needed to use the class + as a process. + A process represents a logical unit in a computational model. + + A process class usually implements: + + - An interface as a set of variables defined as class attributes + (see :func:`variable`, :func:`on_demand`, :func:`foreign` and + :func:`group`). This decorator automatically adds properties to + get/set values for these variables. + + - One or more methods among `initialize()`, `run_step()`, + `finalize_step()` and `finalize()`, which are called at different + stages of a simulation and perform some computation based on the + variables defined in the process interface. + + - Decorated methods to compute, validate or set a default value for one or + more variables. + + Parameters + ---------- + maybe_cls : class, optional + Allows to apply this decorator to a class either as `@process` or + `@process(*args)`. + autodoc : bool, optional + If True, render the docstrings given as a template and fill the + corresponding sections with metadata found in the class + (default: False). + + """ def wrap(cls): attr_cls = _attrify_class(cls) From 418b35adab95407a6ab52046e398f90b65678140 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 8 Mar 2018 20:01:35 +0100 Subject: [PATCH 07/97] allow intent to in, out or inout and maybe set read-only properties TODO: fix write access to foreign variables declared as input in their own process --- xsimlab/process.py | 39 ++++++++++++++++++++++++++++----------- xsimlab/variable.py | 43 ++++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index c7c6d7f4..dca734c6 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -5,7 +5,7 @@ from .variable import AttrType -def get_variables(process, attr_type=None): +def get_variables(process, var_type=None, intent=None): """Helper function that returns variables declared in a process. Useful when one wants to access the variables metadata. @@ -14,9 +14,11 @@ def get_variables(process, attr_type=None): ---------- process : object or class Process class or object. - attr_type : {'variable', 'on_demand', 'foreign', 'group'}, optional + var_type : {'variable', 'on_demand', 'foreign', 'group'}, optional Only return variables of a given kind (by default, return all variables). + intent : {'in', 'out', 'inout'}, optional + Only return input, output or input/output variables. Returns ------- @@ -27,12 +29,16 @@ def get_variables(process, attr_type=None): if not isclass(process): process = process.__class__ - if attr_type is None: - return [field for field in attr.fields(process)] - + if var_type is None: + fields = [f for f in attr.fields(process)] else: - return [field for field in attr.fields(process) - if field.metadata.get('attr_type') == AttrType(attr_type)] + fields = [f for f in attr.fields(process) + if f.metadata.get('attr_type') == AttrType(var_type)] + + if intent is not None: + fields = [f for f in fields if f.metadata.get('intent') == intent] + + return fields def _attrify_class(cls): @@ -59,8 +65,11 @@ def add_obj_attrs(self): def _make_property_variable(var): - """Create a property for a variable.""" + """Create a property for a variable. + The property is read-only if the variable is declared as input. + + """ var_name = var.name def getter(self): @@ -69,7 +78,10 @@ def getter(self): def setter(self, value): self.__xsimlab_store__[(self.__xsimlab_name__, var_name)] = value - return property(fget=getter, fset=setter) + if var.metadata['intent'] == 'in': + return property(fget=getter) + else: + return property(fget=getter, fset=setter) def _make_property_on_demand(var): @@ -87,8 +99,11 @@ def _make_property_on_demand(var): def _make_property_foreign(var): - """Create a property for a foreign variable.""" + """Create a property for a foreign variable. + The property is read-only if the variable is declared as input. + + """ var_name = var.name def getter(self): @@ -102,11 +117,13 @@ def getter(self): def setter(self, value): # no fastpath access (prevent setting read-only variables in store) + # TODO: not working for setting variables that are declared as input + # in their own process!!! o_proc_name, o_var_name = self.__xsimlab_foreign__[var_name] model = self.__xsimlab_model__ return setattr(model._processes[o_proc_name], o_var_name, value) - if var.metadata['intent'] == 'input': + if var.metadata['intent'] == 'in': return property(fget=getter) else: return property(fget=getter, fset=setter) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index 2f657258..a9dc3db7 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -59,13 +59,13 @@ def _as_dim_tuple(dims): def _check_intent(intent): - if intent not in ('input', 'output'): + if intent not in ('in', 'out', 'inout'): raise ValueError("invalid intent given for variable: must be " - "either 'input' or 'output', found '{}'" + "either 'in', 'out' or 'inout', found '{}'" .format(intent)) -def variable(dims=(), intent='input', group=None, default=attr.NOTHING, +def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, validator=None, description='', attrs=None): """Create a variable. @@ -88,16 +88,17 @@ def variable(dims=(), intent='input', group=None, default=attr.NOTHING, a n-d variable. A list of str or tuple items may also be provided if the variable accepts different numbers of dimensions. This should not include a time dimension, which may always be added. - intent : {'input', 'output'}, optional - Defines whether the variable is an input (i.e., a value is needed for - the process computation) or an output (i.e., the process provides a - value for that variable). - (default: 'input'). + intent : {'in', 'out', 'inout'}, optional + Defines whether the variable is an input (i.e., the process needs the + variable's value for its computation), an output (i.e., the process + computes a value for the variable) or both an input/output (i.e., the + process may update the value of the variable). + (default: input/output). group : str, optional Variable group. default : any, optional - Single default value for the variable, ignored when ``intent='output'`` - (default: None). A default value may also be set using a decorator. + Single default value for the variable, ignored when ``intent='out'`` + (default: NOTHING). A default value may also be set using a decorator. validator : callable or list of callable, optional Function that is called at simulation initialization (and possibly at other times too) to check the value given for the variable. @@ -143,8 +144,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): (e.g., using `@myvar.compute` if the name of the variable is `myvar`). - An on-demand variable never accepts an input value - (i.e., intent='output'), and should never be set/updated (read-only). + An on-demand variable is always an output variable (i.e., intent='out'). Its computation usually involves other variables, although this is not required. @@ -174,7 +174,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): """ metadata = {'attr_type': AttrType.ON_DEMAND, 'dims': _as_dim_tuple(dims), - 'intent': 'output', + 'intent': 'out', 'group': group, 'attrs': attrs or {}, 'description': description} @@ -192,7 +192,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): ) -def foreign(other_process_cls, var_name, intent='input'): +def foreign(other_process_cls, var_name, intent='inout'): """Create a reference to a variable that is defined in another process class. @@ -202,11 +202,12 @@ def foreign(other_process_cls, var_name, intent='input'): Class in which the variable is defined. var_name : str Name of the corresponding variable declared in `other_process_cls`. - intent : {'input', 'output'}, optional - Defines whether the variable is an input (i.e., a value is needed for - the process computation) or an output (i.e., the process provides a - value for that variable). - (default: 'input'). + intent : {'in', 'out', 'inout'}, optional + Defines whether the foreign variable is an input (i.e., the process + needs the variable's value for its computation), an output (i.e., the + process computes a value for the variable) or both an input/output + (i.e., the process may update the value of the variable). + (default: input/output). See Also -------- @@ -225,7 +226,7 @@ def group(name): """Create a special variable which value returns an iterable of values of variables in a model that all belong to the same group. - Access to these variable values is read-only (i.e., intent='input'). + Access to the variable values is read-only (i.e., intent='in'). Good examples of using group variables are processes that aggregate (e.g., sum, product, mean) the values of variables that @@ -243,6 +244,6 @@ def group(name): """ metadata = {'attr_type': AttrType.GROUP, 'group': group, - 'intent': 'input'} + 'intent': 'in'} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From 705babb2b6c94a4aa5b9b664045b38bd7c1a7dab Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 9 Mar 2018 16:17:11 +0100 Subject: [PATCH 08/97] better variable properties + rename AttrType to VarType --- xsimlab/process.py | 218 +++++++++++++++++++++++++++----------------- xsimlab/variable.py | 12 ++- 2 files changed, 139 insertions(+), 91 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index dca734c6..ed2cc000 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -2,45 +2,74 @@ import attr -from .variable import AttrType +from .variable import VarType -def get_variables(process, var_type=None, intent=None): - """Helper function that returns variables declared in a process. - - Useful when one wants to access the variables metadata. +def get_variables(process, var_type=None, intent=None, group=None): + """Return (some of) the variables declared in a process. Parameters ---------- process : object or class Process class or object. var_type : {'variable', 'on_demand', 'foreign', 'group'}, optional - Only return variables of a given kind (by default, return all - variables). + Return only variables of a specified type. intent : {'in', 'out', 'inout'}, optional - Only return input, output or input/output variables. + Return only input, output or input/output variables. + group : str, optional + Return only variables that belong to a given group. Returns ------- - attributes : list - A list of :class:`attr.Attribute` objects. + attributes : dict + A dictionary of variable names as keys and :class:`attr.Attribute` + objects as values. """ if not isclass(process): process = process.__class__ - if var_type is None: - fields = [f for f in attr.fields(process)] - else: - fields = [f for f in attr.fields(process) - if f.metadata.get('attr_type') == AttrType(var_type)] + # TODO: use fields_dict instead (attrs 18.1.0) + fields = {a.name: a for a in attr.fields(process)} + + if var_type is not None: + fields = {k: a for k, a in fields.items() + if a.metadata.get('var_type') == VarType(var_type)} if intent is not None: - fields = [f for f in fields if f.metadata.get('intent') == intent] + fields = {k: a for k, a in fields.items() + if a.metadata.get('intent') == intent} + + if group is not None: + fields = {k: a for k, a in fields.items() + if a.metadata.get('group') == group} return fields +def _get_original_variable(var): + """Return the target, original variable of a given variable and + this process class in which the original variable is declared. + + If `var` is not a foreign variable, return itself (and None for + the process). + + In case where the foreign variable point to another foreign + variable (and so on...), this function follow the links until the + original variable is found. + + """ + orig_process_cls = None + orig_var = var + + while orig_var.metadata['var_type'] == VarType.FOREIGN: + orig_process_cls = orig_var.metadata['other_process_cls'] + var_name = orig_var.metadata['var_name'] + orig_var = get_variables(orig_process_cls)[var_name] + + return orig_process_cls, orig_var + + def _attrify_class(cls): """Return a class as if the input class `cls` was decorated with `attr.s`. @@ -48,85 +77,101 @@ def _attrify_class(cls): `attr.s` turns `attr.ib` (or derived) class attributes into fields and adds dunder-methods such as `__init__`. - """ - def add_obj_attrs(self): - """Add instance attributes that are needed later during model creation - or simulation runtime. + The following instance attributes are also defined (values will be + set later at model creation): + + __xsimlab_name__ : str + Name given for this process in a model. + __xsimlab_store__ : dict or object + Simulation data store. + __xsimlab_keys__ : dict + Dictionary that maps variable names to their corresponding key + (or list of keys for group variables) in the store. + Such key consist of pairs like `('foo', 'bar')` where + 'foo' is the name of any process in the same model and 'bar' is + the name of a variable declared in that process. + __xsimlab_od_keys__ : dict + Dictionary that maps variable names to the location of their target + on-demand variable, or None if the target variable is not an on + demand variable), or a list of locations for group variables. + Location here consists of pairs like `(foo_obj, 'bar')`, where + `foo_obj` is any process in the same model 'bar' is the name of a + variable declared in that process. - """ + """ + def init_process(self): self.__xsimlab_name__ = None - self.__xsimlab_model__ = None self.__xsimlab_store__ = None - self.__xsimlab_foreign__ = {} + self.__xsimlab_keys__ = {} + self.__xsimlab_od_keys__ = {} - setattr(cls, '__attrs_post_init__', add_obj_attrs) + setattr(cls, '__attrs_post_init__', init_process) return attr.attrs(cls) def _make_property_variable(var): - """Create a property for a variable. + """Create a property for a variable or a foreign variable. - The property is read-only if the variable is declared as input. + The property get/set functions either read/write values from/to + the simulation data store or compute then get the value of an + on-demand variable. + + The property is read-only if `var` is declared as input or if + `var` is a foreign variable and its target (original) variable is + an on-demand variable. """ var_name = var.name - def getter(self): - return self.__xsimlab_store__[(self.__xsimlab_name__, var_name)] + def get_from_store(self): + key = self.__xsimlab_keys__[var_name] + return self.__xsimlab_store__[key] + + def get_on_demand(self): + od_key = self.__xsimlab_od_keys__[var_name] + return getattr(*od_key) - def setter(self, value): - self.__xsimlab_store__[(self.__xsimlab_name__, var_name)] = value + def put_in_store(self, value): + key = self.__xsimlab_keys__[var_name] + self.__xsimlab_store__[key] = value + + orig_process_cls, orig_var = _get_original_variable(var) + + if orig_var.metadata['var_type'] == VarType.ON_DEMAND: + if var.metadata['intent'] != 'in': + orig_var_str = '.'.join([orig_process_cls.__name__, orig_var.name]) + + raise ValueError( + "variable {} has intent '{}' but its target " + "variable {} is an on-demand variable (read-only)" + .format(var_name, var.metadata['intent'], orig_var_str) + ) + + return property(fget=get_on_demand) + + elif var.metadata['intent'] == 'in': + return property(fget=get_from_store) - if var.metadata['intent'] == 'in': - return property(fget=getter) else: - return property(fget=getter, fset=setter) + return property(fget=get_from_store, fset=put_in_store) def _make_property_on_demand(var): - """Create a read-only property for an on-demand variable.""" + """Create a read-only property for an on-demand variable. + + This property is a simple wrapper around the variable's compute method. + """ if 'compute' not in var.metadata: raise KeyError("no compute method found for on_demand variable " "'{name}': a method decorated with '@{name}.compute' " "is required in the class definition." .format(name=var.name)) - getter = var.metadata['compute'] - - return property(fget=getter) + get_method = var.metadata['compute'] - -def _make_property_foreign(var): - """Create a property for a foreign variable. - - The property is read-only if the variable is declared as input. - - """ - var_name = var.name - - def getter(self): - o_proc_name, o_var_name = self.__xsimlab_foreign__[var_name] - try: - return self.__xsimlab_store__[(o_proc_name, o_var_name)] - except KeyError: - # values of on_demand variables are not in the store - model = self.__xsimlab_model__ - return getattr(model._processes[o_proc_name], o_var_name) - - def setter(self, value): - # no fastpath access (prevent setting read-only variables in store) - # TODO: not working for setting variables that are declared as input - # in their own process!!! - o_proc_name, o_var_name = self.__xsimlab_foreign__[var_name] - model = self.__xsimlab_model__ - return setattr(model._processes[o_proc_name], o_var_name, value) - - if var.metadata['intent'] == 'in': - return property(fget=getter) - else: - return property(fget=getter, fset=setter) + return property(fget=get_method) def _make_property_group(var): @@ -134,16 +179,17 @@ def _make_property_group(var): var_name = var.name - def getter(self): - for o_proc_name, o_var_name in self.__xsimlab_foreign__[var_name]: - try: - yield self.__xsimlab_store__[(o_proc_name, o_var_name)] - except KeyError: - # values of on_demand variables are not in the store - model = self.__xsimlab_model__ - return getattr(model._processes[o_proc_name], o_var_name) + def getter_store_or_on_demand(self): + keys = self.__xsimlab_keys__[var_name] + od_keys = self.__xsimlab_od_keys__[var_name] + + for key, od_key in zip(keys, od_keys): + if od_key is None: + yield self.__xsimlab_store__[key] + else: + yield getattr(*od_key) - return property(fget=getter) + return property(fget=getter_store_or_on_demand) class _ProcessBuilder(object): @@ -154,21 +200,21 @@ class _ProcessBuilder(object): """ _make_prop_funcs = { - AttrType.VARIABLE: _make_property_variable, - AttrType.ON_DEMAND: _make_property_on_demand, - AttrType.FOREIGN: _make_property_foreign, - AttrType.GROUP: _make_property_group + VarType.VARIABLE: _make_property_variable, + VarType.ON_DEMAND: _make_property_on_demand, + VarType.FOREIGN: _make_property_variable, + VarType.GROUP: _make_property_group } def __init__(self, attr_cls): self._cls = attr_cls self._cls_dict = {} - def add_properties(self, attr_type): - make_prop_func = self._make_prop_funcs[attr_type] + def add_properties(self, var_type): + make_prop_func = self._make_prop_funcs[var_type] - for var in get_variables(self._cls, attr_type): - self._cls_dict[var.name] = make_prop_func(var) + for var_name, var in get_variables(self._cls, var_type).items(): + self._cls_dict[var_name] = make_prop_func(var) def render_docstrings(self): self._cls_dict['__doc__'] = "Process-ified class." @@ -220,8 +266,8 @@ def wrap(cls): builder = _ProcessBuilder(attr_cls) - for attr_type in AttrType: - builder.add_properties(attr_type) + for var_type in VarType: + builder.add_properties(var_type) if autodoc: builder.render_docstrings() diff --git a/xsimlab/variable.py b/xsimlab/variable.py index a9dc3db7..c0dae2a6 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -5,7 +5,7 @@ from attr._make import _CountingAttr as CountingAttr_ -class AttrType(Enum): +class VarType(Enum): VARIABLE = 'variable' ON_DEMAND = 'on_demand' FOREIGN = 'foreign' @@ -64,6 +64,8 @@ def _check_intent(intent): "either 'in', 'out' or 'inout', found '{}'" .format(intent)) + return intent + def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, validator=None, description='', attrs=None): @@ -120,7 +122,7 @@ def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, units, math_symbol...). """ - metadata = {'attr_type': AttrType.VARIABLE, + metadata = {'var_type': VarType.VARIABLE, 'dims': _as_dim_tuple(dims), 'intent': _check_intent(intent), 'group': group, @@ -172,7 +174,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): :func:`variable` """ - metadata = {'attr_type': AttrType.ON_DEMAND, + metadata = {'var_type': VarType.ON_DEMAND, 'dims': _as_dim_tuple(dims), 'intent': 'out', 'group': group, @@ -214,7 +216,7 @@ def foreign(other_process_cls, var_name, intent='inout'): :func:`variable` """ - metadata = {'attr_type': AttrType.FOREIGN, + metadata = {'var_type': VarType.FOREIGN, 'other_process_cls': other_process_cls, 'var_name': var_name, 'intent': _check_intent(intent)} @@ -242,7 +244,7 @@ def group(name): :func:`variable` """ - metadata = {'attr_type': AttrType.GROUP, + metadata = {'var_type': VarType.GROUP, 'group': group, 'intent': 'in'} From b28b58f5e12a0b55328d8e95e1a9150cb2064c73 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 9 Mar 2018 19:02:01 +0100 Subject: [PATCH 09/97] rename get_variables to filter_variables + add func argument --- xsimlab/__init__.py | 1 + xsimlab/process.py | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/xsimlab/__init__.py b/xsimlab/__init__.py index 70beaf18..5d837ef1 100644 --- a/xsimlab/__init__.py +++ b/xsimlab/__init__.py @@ -6,6 +6,7 @@ from .variable import variable, on_demand, foreign, group from .process import get_variables, process from .model import Model +from .process import filter_variables, process from ._version import get_versions versions = get_versions() diff --git a/xsimlab/process.py b/xsimlab/process.py index ed2cc000..74b43e9d 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -5,8 +5,9 @@ from .variable import VarType -def get_variables(process, var_type=None, intent=None, group=None): - """Return (some of) the variables declared in a process. +def filter_variables(process, var_type=None, intent=None, group=None, + func=None): + """Filter the variables declared in a process. Parameters ---------- @@ -18,6 +19,10 @@ def get_variables(process, var_type=None, intent=None, group=None): Return only input, output or input/output variables. group : str, optional Return only variables that belong to a given group. + func : callable, optional + A callable that takes a variable (i.e., a :class:`attr.Attribute` + object) as input and return True or False. Useful for more advanced + filtering. Returns ------- @@ -44,6 +49,9 @@ def get_variables(process, var_type=None, intent=None, group=None): fields = {k: a for k, a in fields.items() if a.metadata.get('group') == group} + if func is not None: + fields = {k: a for k, a in fields.items() if func(a)} + return fields @@ -65,17 +73,17 @@ def _get_original_variable(var): while orig_var.metadata['var_type'] == VarType.FOREIGN: orig_process_cls = orig_var.metadata['other_process_cls'] var_name = orig_var.metadata['var_name'] - orig_var = get_variables(orig_process_cls)[var_name] + orig_var = filter_variables(orig_process_cls)[var_name] return orig_process_cls, orig_var def _attrify_class(cls): - """Return a class as if the input class `cls` was decorated with - `attr.s`. + """Return a `cls` after having passed through :func:`attr.attrs`. - `attr.s` turns `attr.ib` (or derived) class attributes into fields - and adds dunder-methods such as `__init__`. + This pulls out and converts `attr.ib` declared as class attributes + into :class:`attr.Attribute` objects and it also adds + dunder-methods such as `__init__`. The following instance attributes are also defined (values will be set later at model creation): @@ -213,7 +221,7 @@ def __init__(self, attr_cls): def add_properties(self, var_type): make_prop_func = self._make_prop_funcs[var_type] - for var_name, var in get_variables(self._cls, var_type).items(): + for var_name, var in filter_variables(self._cls, var_type).items(): self._cls_dict[var_name] = make_prop_func(var) def render_docstrings(self): From 7643840c84a8c14f5214c03a37b7442ee8ddd05a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 9 Mar 2018 19:15:24 +0100 Subject: [PATCH 10/97] minor tweaks --- xsimlab/process.py | 7 ++++--- xsimlab/variable.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 74b43e9d..15b17646 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -122,8 +122,8 @@ def _make_property_variable(var): """Create a property for a variable or a foreign variable. The property get/set functions either read/write values from/to - the simulation data store or compute then get the value of an - on-demand variable. + the simulation data store or get (and trigger computation of) the + value of an on-demand variable. The property is read-only if `var` is declared as input or if `var` is a foreign variable and its target (original) variable is @@ -225,7 +225,8 @@ def add_properties(self, var_type): self._cls_dict[var_name] = make_prop_func(var) def render_docstrings(self): - self._cls_dict['__doc__'] = "Process-ified class." + # self._cls_dict['__doc__'] = "Process-ified class." + raise NotImplementedError("autodoc is not yet implemented.") def build_class(self): cls = self._cls diff --git a/xsimlab/variable.py b/xsimlab/variable.py index c0dae2a6..188d9b7c 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -79,7 +79,7 @@ def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, define the interface of each process in a model. Variables should be declared exclusively as class attributes in - process classes (i.e., classes decorated with `:func:process`). + process classes (i.e., classes decorated with :func:`process`). Parameters ---------- From 34c13eddb74e7d220a746c60330e3627c77094b3 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 9 Mar 2018 20:08:33 +0100 Subject: [PATCH 11/97] start writing what's new entries --- doc/whats_new.rst | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index f31e2cd9..2be47705 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -3,6 +3,63 @@ Release Notes ============= +v0.2.0 (Unreleased) +------------------- + +Highlights +~~~~~~~~~~ + +This release includes a major refactoring of both the internals and +the API on how processes and variables are defined and interact +between each other in a model. xarray-simlab now uses and extends +attrs_ (:issue:`33`). + +.. _attrs: http://www.attrs.org + +Breaking changes +~~~~~~~~~~~~~~~~ + +As xarray-simlab is still at an early development stage and hasn't +been adopted "in production" yet (to our knowledge), we haven't gone +through any depreciation cycle, which by the way would have been +almost impossible for such a major refactoring. The following breaking +changes are effective now! + +- ``Variable``, ``ForeignVariable`` and ``VariableGroup`` classes have + been replaced by ``variable``, ``foreign`` and ``group`` factory + functions (wrappers around ``attr.ib``), respectively. +- ``VariableList`` has been removed and has not been replaced by + anything equivalent. +- ``DiagnosticVariable`` has been replaced by ``on_demand`` and the + ``diagnostic`` decorator has been replaced by the variable's + ``compute`` decorator. +- The ``provided`` (``bool``) argument (variable constructors) has + been replaced by ``intent`` (``{'in', 'out', 'inout'}``). +- The ``allowed_dims`` argument has been renamed to ``dims`` and is + now optional (a scalar value is expected by default). +- The ``validators`` argument has been renamed to ``validator`` to be + consistent with ``attr.ib``. +- The ``optional`` argument has been removed. Variables that don't + require an input value may be defined using a special validator + function (see ``attrs`` documentation). +- Variable values are not anymore accessed using three different + properties ``state``, ``rate`` and ``change`` (e.g., + ``self.foo.state``). Instead, all variables accept a unique value, + which one can get/set by simply using the variable name (e.g., + ``self.foo``). You might want to create different variables to hold + different values. + +- Process classes are now defined using the ``process`` decorator + instead of inheriting from a ``Process`` base class. +- It is not needed anymore to explicitly define whether or not a + process is time dependent (it is now deducted from the methods + implemented in the process class). +- Using ``class Meta`` inside a process class to define some metadata + is not used anymore. + +Enhancements +~~~~~~~~~~~~ + v0.1.1 (20 November 2017) ------------------------- From 1b361c23c7027d44550e2e01a87fedbe702fa970 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 03:02:29 +0100 Subject: [PATCH 12/97] detect cycles when looking for target variable (maybe remove later) --- xsimlab/process.py | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 15b17646..53585966 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -55,27 +55,42 @@ def filter_variables(process, var_type=None, intent=None, group=None, return fields -def _get_original_variable(var): - """Return the target, original variable of a given variable and - this process class in which the original variable is declared. +def get_target_variable(var): + """Return the target (original) variable of a given variable and + the process class in which the target variable is declared. - If `var` is not a foreign variable, return itself (and None for - the process). + If `var` is not a foreign variable, return itself and None instead + of a process class. - In case where the foreign variable point to another foreign - variable (and so on...), this function follow the links until the - original variable is found. + If the target of foreign variable is another foreign variable (and + so on...), this function follow the links until the original + variable is found. An error is thrown if a cyclic pattern is detected. """ - orig_process_cls = None - orig_var = var + target_process_cls = None + target_var = var - while orig_var.metadata['var_type'] == VarType.FOREIGN: - orig_process_cls = orig_var.metadata['other_process_cls'] - var_name = orig_var.metadata['var_name'] - orig_var = filter_variables(orig_process_cls)[var_name] + visited = [] - return orig_process_cls, orig_var + while target_var.metadata['var_type'] == VarType.FOREIGN: + visited.append((target_process_cls, target_var)) + + target_process_cls = target_var.metadata['other_process_cls'] + var_name = target_var.metadata['var_name'] + target_var = filter_variables(target_process_cls)[var_name] + + # TODO: maybe remove this? not even sure such a cycle may happen + # unless we allow later providing other values than classes as first + # argument of `foreign` + if (target_process_cls, target_var) in visited: + cycle = '->'.join(['{}.{}'.format(cls.__name__, var.name) + if cls is not None else var.name + for cls, var in visited]) + + raise RuntimeError("Cycle detected in process dependencies: {}" + .format(cycle)) + + return target_process_cls, target_var def _attrify_class(cls): From b66824826ad3b768470fa38fc871d5e5c5cc2bee Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 03:05:08 +0100 Subject: [PATCH 13/97] perform more sanity checks when creating variable properties --- xsimlab/process.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 53585966..fb703ee6 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -134,15 +134,14 @@ def init_process(self): def _make_property_variable(var): - """Create a property for a variable or a foreign variable. + """Create a property for a variable or a foreign variable (perform + some sanity checks first). The property get/set functions either read/write values from/to the simulation data store or get (and trigger computation of) the value of an on-demand variable. - The property is read-only if `var` is declared as input or if - `var` is a foreign variable and its target (original) variable is - an on-demand variable. + The property is read-only if `var` is declared as input. """ var_name = var.name @@ -159,18 +158,31 @@ def put_in_store(self, value): key = self.__xsimlab_keys__[var_name] self.__xsimlab_store__[key] = value - orig_process_cls, orig_var = _get_original_variable(var) + target_process_cls, target_var = get_target_variable(var) + target_type = target_var.metadata['var_type'] - if orig_var.metadata['var_type'] == VarType.ON_DEMAND: - if var.metadata['intent'] != 'in': - orig_var_str = '.'.join([orig_process_cls.__name__, orig_var.name]) + if target_process_cls is not None: + target_str = '.'.join([target_process_cls.__name__, target_var.name]) + else: + target_str = target_var.name + + intent = var.metadata['intent'] + target_intent = target_var.metadata['intent'] + + if target_type == VarType.GROUP: + raise ValueError("Variable '{var}' links to group variable '{target}', " + "which is not supported. Declare {var} as a group " + "variable instead." + .format(var=var.name, target=target_str)) - raise ValueError( - "variable {} has intent '{}' but its target " - "variable {} is an on-demand variable (read-only)" - .format(var_name, var.metadata['intent'], orig_var_str) - ) + elif (var.metadata['var_type'] == VarType.FOREIGN and + (intent == 'out' and target_intent != 'in' or + target_intent == 'out' and intent != 'in')): + raise ValueError("Incompatible intent given for variables " + "'{}' ('{}') and '{}' ('{}')" + .format(var.name, intent, target_str, target_intent)) + elif target_type == VarType.ON_DEMAND: return property(fget=get_on_demand) elif var.metadata['intent'] == 'in': @@ -187,8 +199,8 @@ def _make_property_on_demand(var): """ if 'compute' not in var.metadata: - raise KeyError("no compute method found for on_demand variable " - "'{name}': a method decorated with '@{name}.compute' " + raise KeyError("No compute method found for on_demand variable " + "'{name}'. A method decorated with '@{name}.compute' " "is required in the class definition." .format(name=var.name)) From 92bae94f861efe678b8c90b7d614ca0573f2322c Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 04:00:49 +0100 Subject: [PATCH 14/97] rename __xsimlab_keys__ to __xsimlab_store_keys__ + fix group keys --- xsimlab/process.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index fb703ee6..8aa6ce58 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -107,16 +107,15 @@ def _attrify_class(cls): Name given for this process in a model. __xsimlab_store__ : dict or object Simulation data store. - __xsimlab_keys__ : dict + __xsimlab_store_keys__ : dict Dictionary that maps variable names to their corresponding key (or list of keys for group variables) in the store. - Such key consist of pairs like `('foo', 'bar')` where + Such keys consist of pairs like `('foo', 'bar')` where 'foo' is the name of any process in the same model and 'bar' is the name of a variable declared in that process. __xsimlab_od_keys__ : dict Dictionary that maps variable names to the location of their target - on-demand variable, or None if the target variable is not an on - demand variable), or a list of locations for group variables. + on-demand variable (or a list of locations for group variables). Location here consists of pairs like `(foo_obj, 'bar')`, where `foo_obj` is any process in the same model 'bar' is the name of a variable declared in that process. @@ -125,7 +124,7 @@ def _attrify_class(cls): def init_process(self): self.__xsimlab_name__ = None self.__xsimlab_store__ = None - self.__xsimlab_keys__ = {} + self.__xsimlab_store_keys__ = {} self.__xsimlab_od_keys__ = {} setattr(cls, '__attrs_post_init__', init_process) @@ -147,7 +146,7 @@ def _make_property_variable(var): var_name = var.name def get_from_store(self): - key = self.__xsimlab_keys__[var_name] + key = self.__xsimlab_store_keys__[var_name] return self.__xsimlab_store__[key] def get_on_demand(self): @@ -155,7 +154,7 @@ def get_on_demand(self): return getattr(*od_key) def put_in_store(self, value): - key = self.__xsimlab_keys__[var_name] + key = self.__xsimlab_store_keys__[var_name] self.__xsimlab_store__[key] = value target_process_cls, target_var = get_target_variable(var) @@ -215,14 +214,14 @@ def _make_property_group(var): var_name = var.name def getter_store_or_on_demand(self): - keys = self.__xsimlab_keys__[var_name] - od_keys = self.__xsimlab_od_keys__[var_name] - - for key, od_key in zip(keys, od_keys): - if od_key is None: - yield self.__xsimlab_store__[key] - else: - yield getattr(*od_key) + store_keys = self.__xsimlab_store_keys__.get(var_name, []) + od_keys = self.__xsimlab_od_keys.get(var_name, []) + + for key in store_keys: + yield self.__xsimlab_store__[key] + + for key in od_keys: + yield getattr(*key) return property(fget=getter_store_or_on_demand) From 35801e55d6c59ef02f1124b0131e247f96f57987 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 12:49:25 +0100 Subject: [PATCH 15/97] docstrings tweaks --- xsimlab/process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 8aa6ce58..678c5e92 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -133,8 +133,8 @@ def init_process(self): def _make_property_variable(var): - """Create a property for a variable or a foreign variable (perform - some sanity checks first). + """Create a property for a variable or a foreign variable (after + some sanity checks). The property get/set functions either read/write values from/to the simulation data store or get (and trigger computation of) the From 1dc85a902f5a59ba1b65a993750798465e05e658 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 12:49:48 +0100 Subject: [PATCH 16/97] change default intent from 'inout' to 'in' for variable and foreign --- xsimlab/variable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index 188d9b7c..9a676834 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -67,7 +67,7 @@ def _check_intent(intent): return intent -def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, +def variable(dims=(), intent='in', group=None, default=attr.NOTHING, validator=None, description='', attrs=None): """Create a variable. @@ -95,7 +95,7 @@ def variable(dims=(), intent='inout', group=None, default=attr.NOTHING, variable's value for its computation), an output (i.e., the process computes a value for the variable) or both an input/output (i.e., the process may update the value of the variable). - (default: input/output). + (default: input). group : str, optional Variable group. default : any, optional @@ -194,7 +194,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): ) -def foreign(other_process_cls, var_name, intent='inout'): +def foreign(other_process_cls, var_name, intent='in'): """Create a reference to a variable that is defined in another process class. @@ -209,7 +209,7 @@ def foreign(other_process_cls, var_name, intent='inout'): needs the variable's value for its computation), an output (i.e., the process computes a value for the variable) or both an input/output (i.e., the process may update the value of the variable). - (default: input/output). + (default: input). See Also -------- From df7bae983c3f036c0b77456705060535d9d6db6a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 12:52:37 +0100 Subject: [PATCH 17/97] WIP refactor Model (add _ModelBuilder class) --- xsimlab/model.py | 199 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 145 insertions(+), 54 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 9691c3ca..8fe98f23 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,25 +1,145 @@ from collections import OrderedDict -from .variable.base import (AbstractVariable, Variable, ForeignVariable, - VariableList, VariableGroup) -from .process import Process +from .variable import VarType +from .process import filter_variables, get_target_variable from .utils import AttrMapping, ContextMixin from .formatting import _calculate_col_width, pretty_print, maybe_truncate -def _set_process_names(processes): - # type: Dict[str, Process] - """Set process names using keys of the mapping.""" - for k, p in processes.items(): - p._name = k +class _ModelBuilder(object): + """Used to iteratively build a new model. + - Reconstruct process/variable dependencies + - Sort processes DAG + - Retrieve model inputs + - Split time dependent vs. independent processes -def _set_group_vars(processes): - """Assign variables that belong to variable groups.""" - for proc in processes.values(): - for var in proc._variables.values(): - if isinstance(var, VariableGroup): - var._set_variables(processes) + """ + def __init__(self, processes_cls): + self._processes_cls = processes_cls + self._processes_obj = {k: cls() for k, cls in processes_cls.items()} + + self._reverse_lookup = {cls: k for k, cls in processes_cls.items()} + + # a cache for group keys + self._group_keys = {} + + def set_process_names(self): + for p_name, p_obj in self.processes_obj.items(): + p_obj.__xsimlab_name__ = p_name + + def _get_var_key(self, p_name, var): + """Get store and on-demand keys for variable `var` declared in + process `p_name`. + + Returned keys are either None (if no key), a tuple or a list + of tuples (for group variables). + + A store key tuple looks like `('foo', 'bar')` where 'foo' is + the name of any process in the model and 'bar' is the name of + a variable declared in that process. + + Similarly, an on-demand key tuple looks like `(foo_obj, 'bar')`, + but where `foo_obj` is a process object rather than its name. + + """ + store_key = None + od_key = None + + var_type = var.metadata['var_type'] + + if var_type == VarType.VARIABLE: + store_key = (p_name, var.name) + + elif var_type == VarType.FOREIGN: + target_p_cls, target_var = get_target_variable(var) + + target_p_name = self._reverse_lookup(target_p_cls) + target_p_obj = self._processes_obj[target_p_name] + + if target_var.metadata['var_type'] == VarType.ON_DEMAND: + od_key = (target_p_obj, target_var.name) + else: + store_key = (target_p_name, target_var.name) + + elif var_type == VarType.GROUP: + group = var.metadata['group'] + + store_key, od_key = self._group_keys.get( + group, self._get_group_var_keys(group) + ) + + return store_key, od_key + + def _get_group_var_keys(self, group): + """Get store and on-demand keys for a group variable.""" + store_keys = [] + od_keys = [] + + for p_name, p_obj in self.processes_obj.items(): + var = filter_variables(p_obj, group=group) + store_key, od_key = self._get_var_key(p_name, var) + + if store_key is not None: + store_keys.append(store_key) + if od_key is not None: + od_keys.append(od_key) + + group_keys = (store_keys, od_keys) + self._group_keys[group] = group_keys + + return group_keys + + def set_process_keys(self): + """Get store and on-demand keys for all variables in a model + and add them in their respective process using the following + attributes: + + __xsimlab_store_keys__ + __xsimlab_od_keys__ + + """ + for p_name, p_obj in self.processes_obj.items(): + for var in filter_variables(p_obj): + store_key, od_key = self._get_var_key(p_name, var) + + if store_key is not None: + p_obj.__xsimlab_store_keys__[var.name] = store_key + if od_key is not None: + p_obj.__xsimlab_od_keys__[var.name] = od_key + + def get_input_variables(self): + """Get all input variables in the model as a list of + `(process_name, var_name)` tuples. + + Model input variables meet the following condition: + + - model-wise (i.e., all processes), there is no variable with + intent='out' that targets those variables (in store keys). + - group variables always have intent='in' but are never model + inputs. + + """ + filter_in = lambda var: ( + var.metadata['var_type'] != VarType.GROUP and + var.metadata['intent'] in ('in', 'inout') + ) + filter_out = lambda var: ( + var.metadata['var_type'] != VarType.ON_DEMAND and + var.metadata['intent'] == 'out' + ) + + in_keys = [] + out_keys = [] + + for p_name, p_obj in self.processes_obj.items(): + in_keys += [p_obj.__xsimlab_store_keys__[var.name] + for var in filter_variables(p_obj, func=filter_in)] + + out_keys += [p_obj.__xsimlab_store_keys__[var.name] + for var in filter_variables(p_obj, func=filter_out)] + + return list(set(in_keys) - set(out_keys)) def _get_foreign_vars(processes): @@ -41,38 +161,6 @@ def _get_foreign_vars(processes): return foreign_vars -def _link_foreign_vars(processes): - """Assign process instances to foreign variables.""" - proc_lookup = {v.__class__: v for v in processes.values()} - - for variables in _get_foreign_vars(processes).values(): - for var in variables: - var._other_process_obj = proc_lookup[var._other_process_cls] - - -def _get_input_vars(processes): - # type: Dict[str, Process] -> Dict[str, Dict[str, Variable]] - - input_vars = {} - - for proc_name, proc in processes.items(): - input_vars[proc_name] = {} - - for k, var in proc._variables.items(): - if isinstance(var, Variable) and not var.provided: - input_vars[proc_name][k] = var - - # case of variables provided by other processes - foreign_vars = _get_foreign_vars(processes) - for variables in foreign_vars.values(): - for var in variables: - if input_vars[var.ref_process.name].get(var.var_name, False): - if var.provided: - del input_vars[var.ref_process.name][var.var_name] - - return {k: v for k, v in input_vars.items() if v} - - def _get_process_dependencies(processes): # type: Dict[str, Process] -> Dict[str, List[Process]] @@ -143,7 +231,7 @@ def _sort_processes(dep_processes): cycle.append(nodes.pop()) cycle.reverse() cycle = '->'.join(cycle) - raise ValueError( + raise RuntimeError( "cycle detected in process graph: %s" % cycle ) next_nodes.append(nxt) @@ -160,8 +248,8 @@ def _sort_processes(dep_processes): class Model(AttrMapping, ContextMixin): - """An immutable collection (mapping) of process units that together - form a computational model. + """An immutable collection of process units that together form a + computational model. This collection is ordered such that the computational flow is consistent with process inter-dependencies. @@ -170,7 +258,7 @@ class Model(AttrMapping, ContextMixin): computed using the processes interfaces. Processes interfaces are also used for automatically retrieving - the model inputs, i.e., all the variables which require setting a + the model inputs, i.e., all the variables that require setting a value before running the model. """ @@ -179,15 +267,18 @@ def __init__(self, processes): Parameters ---------- processes : dict - Dictionnary with process names as keys and subclasses of - `Process` as values. + Dictionnary with process names as keys and classes (decorated with + :func:`process`) as values. """ processes_obj = {} for k, cls in processes.items(): - if not issubclass(cls, Process) or cls is Process: - raise TypeError("%s is not a subclass of Process" % cls) + if getattr(cls, '__xsimlab_name__') is None: + raise TypeError("class {} is not a process compatible class: " + "you might want decorate it first using " + "@process".format(cls.__name__)) + processes_obj[k] = cls() _set_process_names(processes_obj) From 72653e8684997f2a7bb061f39abdcec5ec7c381c Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 13:05:00 +0100 Subject: [PATCH 18/97] fix handling of on-demand variables in get_input_variables --- xsimlab/model.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 8fe98f23..a6e7db13 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -114,32 +114,28 @@ def get_input_variables(self): Model input variables meet the following condition: - - model-wise (i.e., all processes), there is no variable with + - model-wise (i.e., in all processes), there is no variable with intent='out' that targets those variables (in store keys). - - group variables always have intent='in' but are never model - inputs. + - although group variables always have intent='in', they are not + model inputs. """ filter_in = lambda var: ( var.metadata['var_type'] != VarType.GROUP and var.metadata['intent'] in ('in', 'inout') ) - filter_out = lambda var: ( - var.metadata['var_type'] != VarType.ON_DEMAND and - var.metadata['intent'] == 'out' - ) in_keys = [] out_keys = [] for p_name, p_obj in self.processes_obj.items(): - in_keys += [p_obj.__xsimlab_store_keys__[var.name] + in_keys += [p_obj.__xsimlab_store_keys__.get(var.name) for var in filter_variables(p_obj, func=filter_in)] - out_keys += [p_obj.__xsimlab_store_keys__[var.name] - for var in filter_variables(p_obj, func=filter_out)] + out_keys += [p_obj.__xsimlab_store_keys__.get(var.name) + for var in filter_variables(p_obj, intent='out')] - return list(set(in_keys) - set(out_keys)) + return [k for k in set(in_keys) - set(out_keys) if k is not None] def _get_foreign_vars(processes): From 421cb4ebccbadea4b038b04bba6bb9ab2d032d1b Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 13:55:31 +0100 Subject: [PATCH 19/97] use Enum for variable intent --- xsimlab/model.py | 20 ++++++++++++-------- xsimlab/process.py | 13 +++++++------ xsimlab/variable.py | 23 ++++++++++------------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index a6e7db13..897c7a92 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from .variable import VarType +from .variable import VarIntent, VarType from .process import filter_variables, get_target_variable from .utils import AttrMapping, ContextMixin from .formatting import _calculate_col_width, pretty_print, maybe_truncate @@ -112,7 +112,7 @@ def get_input_variables(self): """Get all input variables in the model as a list of `(process_name, var_name)` tuples. - Model input variables meet the following condition: + Model input variables meet the following conditions: - model-wise (i.e., in all processes), there is no variable with intent='out' that targets those variables (in store keys). @@ -122,18 +122,22 @@ def get_input_variables(self): """ filter_in = lambda var: ( var.metadata['var_type'] != VarType.GROUP and - var.metadata['intent'] in ('in', 'inout') + var.metadata['intent'] in (VarIntent.IN, VarIntent.INOUT) ) in_keys = [] out_keys = [] for p_name, p_obj in self.processes_obj.items(): - in_keys += [p_obj.__xsimlab_store_keys__.get(var.name) - for var in filter_variables(p_obj, func=filter_in)] - - out_keys += [p_obj.__xsimlab_store_keys__.get(var.name) - for var in filter_variables(p_obj, intent='out')] + in_keys += [ + p_obj.__xsimlab_store_keys__.get(var.name) + for var in filter_variables(p_obj, func=filter_in) + ] + + out_keys += [ + p_obj.__xsimlab_store_keys__.get(var.name) + for var in filter_variables(p_obj, intent=VarIntent.OUT) + ] return [k for k in set(in_keys) - set(out_keys) if k is not None] diff --git a/xsimlab/process.py b/xsimlab/process.py index 678c5e92..19d7cb56 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -2,7 +2,7 @@ import attr -from .variable import VarType +from .variable import VarIntent, VarType def filter_variables(process, var_type=None, intent=None, group=None, @@ -43,7 +43,7 @@ def filter_variables(process, var_type=None, intent=None, group=None, if intent is not None: fields = {k: a for k, a in fields.items() - if a.metadata.get('intent') == intent} + if a.metadata.get('intent') == VarIntent(intent)} if group is not None: fields = {k: a for k, a in fields.items() @@ -175,16 +175,17 @@ def put_in_store(self, value): .format(var=var.name, target=target_str)) elif (var.metadata['var_type'] == VarType.FOREIGN and - (intent == 'out' and target_intent != 'in' or - target_intent == 'out' and intent != 'in')): + (intent == VarIntent.OUT and target_intent != VarIntent.IN or + target_intent == VarIntent.OUT and intent != VarIntent.IN)): raise ValueError("Incompatible intent given for variables " "'{}' ('{}') and '{}' ('{}')" - .format(var.name, intent, target_str, target_intent)) + .format(var.name, intent.value, + target_str, target_intent.value)) elif target_type == VarType.ON_DEMAND: return property(fget=get_on_demand) - elif var.metadata['intent'] == 'in': + elif var.metadata['intent'] == VarIntent.IN: return property(fget=get_from_store) else: diff --git a/xsimlab/variable.py b/xsimlab/variable.py index 9a676834..80777cb8 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -12,6 +12,12 @@ class VarType(Enum): GROUP = 'group' +class VarIntent(Enum): + IN = 'in' + OUT = 'out' + INOUT = 'inout' + + class _CountingAttr(CountingAttr_): """A hack to add a custom 'compute' decorator for on-request computation of on_demand variables. @@ -58,15 +64,6 @@ def _as_dim_tuple(dims): return tuple(dims) -def _check_intent(intent): - if intent not in ('in', 'out', 'inout'): - raise ValueError("invalid intent given for variable: must be " - "either 'in', 'out' or 'inout', found '{}'" - .format(intent)) - - return intent - - def variable(dims=(), intent='in', group=None, default=attr.NOTHING, validator=None, description='', attrs=None): """Create a variable. @@ -124,7 +121,7 @@ def variable(dims=(), intent='in', group=None, default=attr.NOTHING, """ metadata = {'var_type': VarType.VARIABLE, 'dims': _as_dim_tuple(dims), - 'intent': _check_intent(intent), + 'intent': VarIntent(intent), 'group': group, 'attrs': attrs or {}, 'description': description} @@ -176,7 +173,7 @@ def on_demand(dims=(), group=None, description='', attrs=None): """ metadata = {'var_type': VarType.ON_DEMAND, 'dims': _as_dim_tuple(dims), - 'intent': 'out', + 'intent': VarIntent.OUT, 'group': group, 'attrs': attrs or {}, 'description': description} @@ -219,7 +216,7 @@ def foreign(other_process_cls, var_name, intent='in'): metadata = {'var_type': VarType.FOREIGN, 'other_process_cls': other_process_cls, 'var_name': var_name, - 'intent': _check_intent(intent)} + 'intent': VarIntent(intent)} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) @@ -246,6 +243,6 @@ def group(name): """ metadata = {'var_type': VarType.GROUP, 'group': group, - 'intent': 'in'} + 'intent': VarIntent.IN} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From 3fd166fedb59d41d1f9b0f5f2b5757a24c555eb3 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 11 Mar 2018 16:07:51 +0100 Subject: [PATCH 20/97] WIP model builder (process dependencies and sorting) --- xsimlab/model.py | 231 ++++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 114 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 897c7a92..594d6f5e 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,5 +1,7 @@ from collections import OrderedDict +import attr + from .variable import VarIntent, VarType from .process import filter_variables, get_target_variable from .utils import AttrMapping, ContextMixin @@ -21,6 +23,8 @@ def __init__(self, processes_cls): self._reverse_lookup = {cls: k for k, cls in processes_cls.items()} + self._dep_processes = {k: set() for k in self._processes_obj} + # a cache for group keys self._group_keys = {} @@ -72,20 +76,21 @@ def _get_var_key(self, p_name, var): return store_key, od_key def _get_group_var_keys(self, group): - """Get store and on-demand keys for a group variable.""" + """Get all store and on-demand keys related to a group variable.""" store_keys = [] od_keys = [] for p_name, p_obj in self.processes_obj.items(): - var = filter_variables(p_obj, group=group) - store_key, od_key = self._get_var_key(p_name, var) + for var in filter_variables(p_obj, group=group).values(): + store_key, od_key = self._get_var_key(p_name, var) - if store_key is not None: - store_keys.append(store_key) - if od_key is not None: - od_keys.append(od_key) + if store_key is not None: + store_keys.append(store_key) + if od_key is not None: + od_keys.append(od_key) group_keys = (store_keys, od_keys) + self._group_keys[group] = group_keys return group_keys @@ -100,7 +105,7 @@ def set_process_keys(self): """ for p_name, p_obj in self.processes_obj.items(): - for var in filter_variables(p_obj): + for var in filter_variables(p_obj).values(): store_key, od_key = self._get_var_key(p_name, var) if store_key is not None: @@ -124,6 +129,7 @@ def get_input_variables(self): var.metadata['var_type'] != VarType.GROUP and var.metadata['intent'] in (VarIntent.IN, VarIntent.INOUT) ) + filter_out = lambda var: var.metadata['intent'] == VarIntent.OUT in_keys = [] out_keys = [] @@ -131,120 +137,128 @@ def get_input_variables(self): for p_name, p_obj in self.processes_obj.items(): in_keys += [ p_obj.__xsimlab_store_keys__.get(var.name) - for var in filter_variables(p_obj, func=filter_in) + for var in filter_variables(p_obj, func=filter_in).values() ] - out_keys += [ p_obj.__xsimlab_store_keys__.get(var.name) - for var in filter_variables(p_obj, intent=VarIntent.OUT) + for var in filter_variables(p_obj, intent=filter_out).values() ] return [k for k in set(in_keys) - set(out_keys) if k is not None] + def _add_dependency(self, p_name, p_obj, var_name, key): + # case of group variable + if isinstance(key, list): + for k in key: + self._add_dependency(p_name, p_obj, var_name, k) -def _get_foreign_vars(processes): - # type: Dict[str, Process] -> Dict[str, List[ForeignVariable]] + else: + p_target, _ = key - foreign_vars = {} + if not isinstance(p_target, str): + # case of on-demand target variable + p_target_name = self._reverse_lookup[type(p_target)] - for proc_name, proc in processes.items(): - foreign_vars[proc_name] = [] + self._dep_processes[p_name].add(p_target_name) + return - for var in proc._variables.values(): - if isinstance(var, (tuple, list, VariableGroup)): - foreign_vars[proc_name] += [ - v for v in var if isinstance(v, ForeignVariable) - ] - elif isinstance(var, ForeignVariable): - foreign_vars[proc_name].append(var) + else: + p_target_name = p_target - return foreign_vars + var = attr.fields(type(p_obj))[var_name] + if var.metadata['intent'] == VarIntent.OUT: + self._dep_processes[p_target_name].add(p_name) + else: + self._dep_processes[p_name].add(p_target_name) -def _get_process_dependencies(processes): - # type: Dict[str, Process] -> Dict[str, List[Process]] + def get_process_dependencies(self): + for p_name, p_obj in self.processes_obj.items(): - dep_processes = {k: set() for k in processes} - foreign_vars = _get_foreign_vars(processes) + store_keys = p_obj.__xsimlab_store_keys__ + od_keys = p_obj.__xsimlab_od_keys__ - for proc_name, variables in foreign_vars.items(): - for var in variables: - if var.provided: - dep_processes[var.ref_process.name].add(proc_name) - else: - ref_var = var.ref_var - if ref_var.provided or getattr(ref_var, 'optional', False): - dep_processes[proc_name].add(var.ref_process.name) + for var_name, key in store_keys.items(): + self._add_dependency(p_name, p_obj, var_name, key) - return {k: list(v) for k, v in dep_processes.items()} + for var_name, key in od_keys.items(): + self._add_dependency(p_name, p_obj, var_name, key) + self._dep_processes = {k: list(v) for k, v in self._dep_processes} + return self._dep_processes -def _sort_processes(dep_processes): - # type: Dict[str, List[Process]] -> List[str] - """Stack-based depth-first search traversal. + def sort_processes(self): + # type: Dict[str, List[Process]] -> List[str] + """Stack-based depth-first search traversal. - This is based on Tarjan's method for topological sorting. + This is based on Tarjan's method for topological sorting. - Part of the code below is copied and modified from: + Part of the code below is copied and modified from: - - dask 0.14.3 (Copyright (c) 2014-2015, Continuum Analytics, Inc. - and contributors) - Licensed under the BSD 3 License - http://dask.pydata.org + - dask 0.14.3 (Copyright (c) 2014-2015, Continuum Analytics, Inc. + and contributors) + Licensed under the BSD 3 License + http://dask.pydata.org - """ - ordered = [] - - # Nodes whose descendents have been completely explored. - # These nodes are guaranteed to not be part of a cycle. - completed = set() - - # All nodes that have been visited in the current traversal. Because - # we are doing depth-first search, going "deeper" should never result - # in visiting a node that has already been seen. The `seen` and - # `completed` sets are mutually exclusive; it is okay to visit a node - # that has already been added to `completed`. - seen = set() - - for key in dep_processes: - if key in completed: - continue - nodes = [key] - while nodes: - # Keep current node on the stack until all descendants are visited - cur = nodes[-1] - if cur in completed: - # Already fully traversed descendants of cur - nodes.pop() + """ + ordered = [] + + # Nodes whose descendents have been completely explored. + # These nodes are guaranteed to not be part of a cycle. + completed = set() + + # All nodes that have been visited in the current traversal. Because + # we are doing depth-first search, going "deeper" should never result + # in visiting a node that has already been seen. The `seen` and + # `completed` sets are mutually exclusive; it is okay to visit a node + # that has already been added to `completed`. + seen = set() + + for key in self._dep_processes: + if key in completed: continue - seen.add(cur) - - # Add direct descendants of cur to nodes stack - next_nodes = [] - for nxt in dep_processes[cur]: - if nxt not in completed: - if nxt in seen: - # Cycle detected! - cycle = [nxt] - while nodes[-1] != nxt: + nodes = [key] + while nodes: + # Keep current node on the stack until all descendants are + # visited + cur = nodes[-1] + if cur in completed: + # Already fully traversed descendants of cur + nodes.pop() + continue + seen.add(cur) + + # Add direct descendants of cur to nodes stack + next_nodes = [] + for nxt in self._dep_processes[cur]: + if nxt not in completed: + if nxt in seen: + # Cycle detected! + cycle = [nxt] + while nodes[-1] != nxt: + cycle.append(nodes.pop()) cycle.append(nodes.pop()) - cycle.append(nodes.pop()) - cycle.reverse() - cycle = '->'.join(cycle) - raise RuntimeError( - "cycle detected in process graph: %s" % cycle - ) - next_nodes.append(nxt) - - if next_nodes: - nodes.extend(next_nodes) - else: - # cur has no more descendants to explore, so we're done with it - ordered.append(cur) - completed.add(cur) - seen.remove(cur) - nodes.pop() - return ordered + cycle.reverse() + cycle = '->'.join(cycle) + raise RuntimeError( + "Cycle detected in process graph: %s" % cycle + ) + next_nodes.append(nxt) + + if next_nodes: + nodes.extend(next_nodes) + else: + # cur has no more descendants to explore, + # so we're done with it + ordered.append(cur) + completed.add(cur) + seen.remove(cur) + nodes.pop() + return ordered + + def get_processes(self): + return OrderedDict((p_name, self._processes_obj[p_name]) + for p_name in self.sort_processes()) class Model(AttrMapping, ContextMixin): @@ -271,26 +285,15 @@ def __init__(self, processes): :func:`process`) as values. """ - processes_obj = {} + builder = _ModelBuilder(processes) - for k, cls in processes.items(): - if getattr(cls, '__xsimlab_name__') is None: - raise TypeError("class {} is not a process compatible class: " - "you might want decorate it first using " - "@process".format(cls.__name__)) + builder.set_process_names() + builder.set_process_keys() - processes_obj[k] = cls() + self._input_vars = builder.get_input_variables() + self._dep_processes = builder.get_process_dependencies() + self._processes = builder.get_processes() - _set_process_names(processes_obj) - _set_group_vars(processes_obj) - _link_foreign_vars(processes_obj) - - self._input_vars = _get_input_vars(processes_obj) - self._dep_processes = _get_process_dependencies(processes_obj) - self._processes = OrderedDict( - [(k, processes_obj[k]) - for k in _sort_processes(self._dep_processes)] - ) self._time_processes = OrderedDict( [(k, proc) for k, proc in self._processes.items() if proc.meta['time_dependent']] From fbd606e2309084a237d3761c9aaf8606f2fcaddb Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 12 Mar 2018 10:53:35 +0100 Subject: [PATCH 21/97] change intent rules for foreign variables --- xsimlab/process.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 19d7cb56..d3fb45e3 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -158,34 +158,39 @@ def put_in_store(self, value): self.__xsimlab_store__[key] = value target_process_cls, target_var = get_target_variable(var) + + var_type = var.metadata['var_type'] target_type = target_var.metadata['var_type'] + var_intent = var.metadata['intent'] + target_intent = target_var.metadata['intent'] if target_process_cls is not None: target_str = '.'.join([target_process_cls.__name__, target_var.name]) else: target_str = target_var.name - intent = var.metadata['intent'] - target_intent = target_var.metadata['intent'] - if target_type == VarType.GROUP: raise ValueError("Variable '{var}' links to group variable '{target}', " "which is not supported. Declare {var} as a group " "variable instead." .format(var=var.name, target=target_str)) - elif (var.metadata['var_type'] == VarType.FOREIGN and - (intent == VarIntent.OUT and target_intent != VarIntent.IN or - target_intent == VarIntent.OUT and intent != VarIntent.IN)): + elif (var_type == VarType.FOREIGN and + var_intent == VarIntent.OUT and target_intent == VarIntent.OUT): raise ValueError("Incompatible intent given for variables " "'{}' ('{}') and '{}' ('{}')" - .format(var.name, intent.value, + .format(var.name, var_intent.value, target_str, target_intent.value)) elif target_type == VarType.ON_DEMAND: + if var_intent in (VarIntent.OUT, VarIntent.INOUT): + raise ValueError("Variable '{}' targeting on-demand variable " + "'{}' should have intent='in' (found '{}')" + .format(var.name, target_str, var_intent.value)) + return property(fget=get_on_demand) - elif var.metadata['intent'] == VarIntent.IN: + elif var_type == VarIntent.IN: return property(fget=get_from_store) else: From 335b6a06c267fd8b679d0d756aae618bb0a566f0 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 12 Mar 2018 14:54:28 +0100 Subject: [PATCH 22/97] complete ModelBuilder implementation --- xsimlab/model.py | 182 +++++++++++++++++++++++++++++------------------ 1 file changed, 113 insertions(+), 69 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 594d6f5e..d3f562c7 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,7 +1,5 @@ from collections import OrderedDict -import attr - from .variable import VarIntent, VarType from .process import filter_variables, get_target_variable from .utils import AttrMapping, ContextMixin @@ -11,9 +9,9 @@ class _ModelBuilder(object): """Used to iteratively build a new model. - - Reconstruct process/variable dependencies - - Sort processes DAG + - Define variable keys in store - Retrieve model inputs + - Reconstruct process dependencies and sort DAG of processes - Split time dependent vs. independent processes """ @@ -23,20 +21,21 @@ def __init__(self, processes_cls): self._reverse_lookup = {cls: k for k, cls in processes_cls.items()} - self._dep_processes = {k: set() for k in self._processes_obj} + self._dep_processes = None + self._sorted_processes = None # a cache for group keys self._group_keys = {} def set_process_names(self): - for p_name, p_obj in self.processes_obj.items(): + for p_name, p_obj in self._processes_obj.items(): p_obj.__xsimlab_name__ = p_name def _get_var_key(self, p_name, var): - """Get store and on-demand keys for variable `var` declared in + """Get store and/or on-demand keys for variable `var` declared in process `p_name`. - Returned keys are either None (if no key), a tuple or a list + Returned key(s) are either None (if no key), a tuple or a list of tuples (for group variables). A store key tuple looks like `('foo', 'bar')` where 'foo' is @@ -58,7 +57,7 @@ def _get_var_key(self, p_name, var): elif var_type == VarType.FOREIGN: target_p_cls, target_var = get_target_variable(var) - target_p_name = self._reverse_lookup(target_p_cls) + target_p_name = self._reverse_lookup[target_p_cls] target_p_obj = self._processes_obj[target_p_name] if target_var.metadata['var_type'] == VarType.ON_DEMAND: @@ -67,21 +66,27 @@ def _get_var_key(self, p_name, var): store_key = (target_p_name, target_var.name) elif var_type == VarType.GROUP: - group = var.metadata['group'] - - store_key, od_key = self._group_keys.get( - group, self._get_group_var_keys(group) - ) + var_group = var.metadata['group'] + store_key, od_key = self._get_group_var_keys(var_group) return store_key, od_key def _get_group_var_keys(self, group): - """Get all store and on-demand keys related to a group variable.""" + """Get from cache or find model-wise store and on-demand keys + for all variables related to a group (except group variables). + + """ + if group in self._group_keys: + return self._group_keys[group] + store_keys = [] od_keys = [] - for p_name, p_obj in self.processes_obj.items(): + for p_name, p_obj in self._processes_obj.items(): for var in filter_variables(p_obj, group=group).values(): + if var.metadata['var_type'] == VarType.GROUP: + continue + store_key, od_key = self._get_var_key(p_name, var) if store_key is not None: @@ -89,22 +94,20 @@ def _get_group_var_keys(self, group): if od_key is not None: od_keys.append(od_key) - group_keys = (store_keys, od_keys) + self._group_keys[group] = store_keys, od_keys - self._group_keys[group] = group_keys - - return group_keys + return store_keys, od_keys def set_process_keys(self): - """Get store and on-demand keys for all variables in a model - and add them in their respective process using the following + """Find store and/or on-demand keys for all variables in a model and + store them in their respective process, i.e., the following attributes: - __xsimlab_store_keys__ - __xsimlab_od_keys__ + __xsimlab_store_keys__ (store keys) + __xsimlab_od_keys__ (on-demand keys) """ - for p_name, p_obj in self.processes_obj.items(): + for p_name, p_obj in self._processes_obj.items(): for var in filter_variables(p_obj).values(): store_key, od_key = self._get_var_key(p_name, var) @@ -120,7 +123,7 @@ def get_input_variables(self): Model input variables meet the following conditions: - model-wise (i.e., in all processes), there is no variable with - intent='out' that targets those variables (in store keys). + intent='out' targeting those variables (in store keys). - although group variables always have intent='in', they are not model inputs. @@ -134,62 +137,88 @@ def get_input_variables(self): in_keys = [] out_keys = [] - for p_name, p_obj in self.processes_obj.items(): + for p_name, p_obj in self._processes_obj.items(): in_keys += [ p_obj.__xsimlab_store_keys__.get(var.name) for var in filter_variables(p_obj, func=filter_in).values() ] out_keys += [ p_obj.__xsimlab_store_keys__.get(var.name) - for var in filter_variables(p_obj, intent=filter_out).values() + for var in filter_variables(p_obj, func=filter_out).values() ] return [k for k in set(in_keys) - set(out_keys) if k is not None] - def _add_dependency(self, p_name, p_obj, var_name, key): - # case of group variable + def _maybe_add_dependency(self, p_name, p_obj, var_name, key): + """Maybe add a process dependency based on single variable + `var_name`, defined in process `p_name`/`p_obj`, with the + corresponding `key` (either store or on-demand key). + + A process depends on another process if it has a variable (or + foreign) for which the other process declares a foreign (or + variable) that provides a value (i.e., intent='out'). + + """ if isinstance(key, list): + # group variable for k in key: - self._add_dependency(p_name, p_obj, var_name, k) + self._maybe_add_dependency(p_name, p_obj, var_name, k) else: - p_target, _ = key + target_p, target_var_name = key - if not isinstance(p_target, str): - # case of on-demand target variable - p_target_name = self._reverse_lookup[type(p_target)] + if not isinstance(target_p, str): + # on-demand target variable + target_p_name = self._reverse_lookup[type(target_p)] + target_p_obj = target_p + else: + target_p_name = target_p + target_p_obj = self._processes_obj[target_p_name] - self._dep_processes[p_name].add(p_target_name) - return + var = filter_variables(p_obj)[var_name] + target_var = filter_variables(target_p_obj)[target_var_name] - else: - p_target_name = p_target + if target_p_name == p_name: + # not a foreign variable + pass - var = attr.fields(type(p_obj))[var_name] + elif var.metadata['intent'] == VarIntent.OUT: + # target process depends on current process + self._dep_processes[target_p_name].add(p_name) - if var.metadata['intent'] == VarIntent.OUT: - self._dep_processes[p_target_name].add(p_name) - else: - self._dep_processes[p_name].add(p_target_name) + elif target_var.metadata['intent'] == VarIntent.OUT: + # current process depends on target process + self._dep_processes[p_name].add(target_p_name) def get_process_dependencies(self): - for p_name, p_obj in self.processes_obj.items(): + """Return a dictionary where keys are each process of the model and + values are lists of dependent processes (or empty lists for processes + that have no dependencies). + + """ + self._dep_processes = {k: set() for k in self._processes_obj} + + for p_name, p_obj in self._processes_obj.items(): store_keys = p_obj.__xsimlab_store_keys__ od_keys = p_obj.__xsimlab_od_keys__ for var_name, key in store_keys.items(): - self._add_dependency(p_name, p_obj, var_name, key) + self._maybe_add_dependency(p_name, p_obj, var_name, key) for var_name, key in od_keys.items(): - self._add_dependency(p_name, p_obj, var_name, key) + self._maybe_add_dependency(p_name, p_obj, var_name, key) + + self._dep_processes = {k: list(v) + for k, v in self._dep_processes.items()} - self._dep_processes = {k: list(v) for k, v in self._dep_processes} return self._dep_processes - def sort_processes(self): - # type: Dict[str, List[Process]] -> List[str] - """Stack-based depth-first search traversal. + def _sort_processes(self): + """Sort processes based on their dependencies (return a list of sorted + process names). + + Stack-based depth-first search traversal. This is based on Tarjan's method for topological sorting. @@ -256,9 +285,28 @@ def sort_processes(self): nodes.pop() return ordered - def get_processes(self): - return OrderedDict((p_name, self._processes_obj[p_name]) - for p_name in self.sort_processes()) + def get_sorted_processes(self): + self._sorted_processes = OrderedDict( + [(p_name, self._processes_obj[p_name]) + for p_name in self._sort_processes()] + ) + return self._sorted_processes + + def get_time_processes(self): + """Time processes are process classes that implement `run_step` + and/or `finalize_step` method(s). + + """ + has_method = lambda obj, meth: callable(getattr(obj, meth, None)) + + is_time_process = lambda obj: (has_method(obj, 'run_step') or + has_method(obj, 'finalize_step')) + + return OrderedDict([ + (p_name, p_obj) + for p_name, p_obj in self._sorted_processes.items() + if is_time_process(p_obj) + ]) class Model(AttrMapping, ContextMixin): @@ -292,12 +340,8 @@ def __init__(self, processes): self._input_vars = builder.get_input_variables() self._dep_processes = builder.get_process_dependencies() - self._processes = builder.get_processes() - - self._time_processes = OrderedDict( - [(k, proc) for k, proc in self._processes.items() - if proc.meta['time_dependent']] - ) + self._processes = builder.get_sorted_processes() + self._time_processes = builder.get_time_processes() super(Model, self).__init__(self._processes) self._initialized = True @@ -335,15 +379,15 @@ def is_input(self, variable): True if the variable is a input of Model (otherwise False). """ - if isinstance(variable, AbstractVariable): - proc_name, var_name = self._get_proc_var_name(variable) - elif isinstance(variable, (VariableList, VariableGroup)): - proc_name, var_name = None, None # prevent unpack iterable below - else: - proc_name, var_name = variable - - if self._input_vars.get(proc_name, {}).get(var_name, False): - return True + # if isinstance(variable, AbstractVariable): + # proc_name, var_name = self._get_proc_var_name(variable) + # elif isinstance(variable, (VariableList, VariableGroup)): + # proc_name, var_name = None, None # prevent unpack iterable below + # else: + # proc_name, var_name = variable + + # if self._input_vars.get(proc_name, {}).get(var_name, False): + # return True return False def visualize(self, show_only_variable=None, show_inputs=False, From e4614520204d04ff0080a7164b0a70497409fdc2 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 12:48:18 +0200 Subject: [PATCH 23/97] fix variable group --- xsimlab/variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index 80777cb8..ff349aa6 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -242,7 +242,7 @@ def group(name): """ metadata = {'var_type': VarType.GROUP, - 'group': group, + 'group': name, 'intent': VarIntent.IN} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From f15ef9b9bf9defa0821bafd4d554806e66a6f1f5 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 12:49:50 +0200 Subject: [PATCH 24/97] update Model input variables API --- doc/whats_new.rst | 8 +++++ xsimlab/model.py | 75 ++++++++++++++++++++++++----------------------- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 2be47705..bc8f7353 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -57,6 +57,14 @@ changes are effective now! - Using ``class Meta`` inside a process class to define some metadata is not used anymore. +- ``Model.is_input`` no longer accepts a Variable object as + argument. Instead, it accepts two arguments (process name and + variable name). +- ``Model.input_vars`` now returns a list of ``(process_name, + variable_name)`` tuples instead of a dict of + dicts. ``Model.input_vars_dict`` has been added for convenience + (i.e., to get input variables grouped by process as a dictionary). + Enhancements ~~~~~~~~~~~~ diff --git a/xsimlab/model.py b/xsimlab/model.py index d3f562c7..52ef707b 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +from collections import OrderedDict, defaultdict from .variable import VarIntent, VarType from .process import filter_variables, get_target_variable @@ -339,6 +339,8 @@ def __init__(self, processes): builder.set_process_keys() self._input_vars = builder.get_input_variables() + self._input_vars_dict = None + self._dep_processes = builder.get_process_dependencies() self._processes = builder.get_sorted_processes() self._time_processes = builder.get_time_processes() @@ -346,49 +348,54 @@ def __init__(self, processes): super(Model, self).__init__(self._processes) self._initialized = True - def _get_proc_var_name(self, variable): - # type: AbstractVariable -> Union[tuple[str, str], None] - for proc_name, variables in self._processes.items(): - for var_name, var in variables.items(): - if var is variable: - return proc_name, var_name - return None, None - @property def input_vars(self): - """Returns all variables that require setting a - value before running the model. + """Returns all variables that require setting a value before running + the model. - These variables are grouped by process name (dict of dicts). + A list of `(process_name, var_name)` tuples (or an empty list) + is returned. """ return self._input_vars - def is_input(self, variable): + @property + def input_vars_dict(self): + """Returns all variables that require setting a value before running + the model. + + Unlike `input_vars` property, a dictionary of lists of variable names + grouped by process is returned. + + """ + if self._input_vars_dict is None: + inputs = defaultdict(list) + + for proc_name, var_name in self._input_vars: + inputs[proc_name].append(var_name) + + self._input_vars_dict = dict(inputs) + + return self._input_vars_dict + + def is_input(self, proc_name, var_name): """Test if a variable is an input of Model. Parameters ---------- - variable : object or tuple - Either a Variable object or a (str, str) tuple - corresponding to process name and variable name. + proc_name : str + Name of a process. + var_name : str + Name of a variable declared in that process. Returns ------- is_input : bool - True if the variable is a input of Model (otherwise False). + True if the variable is a input of Model (otherwise False, + even when `(proc_name, var_name)` doesn't exist in Model). """ - # if isinstance(variable, AbstractVariable): - # proc_name, var_name = self._get_proc_var_name(variable) - # elif isinstance(variable, (VariableList, VariableGroup)): - # proc_name, var_name = None, None # prevent unpack iterable below - # else: - # proc_name, var_name = variable - - # if self._input_vars.get(proc_name, {}).get(var_name, False): - # return True - return False + return (proc_name, var_name) in self._input_vars def visualize(self, show_only_variable=None, show_inputs=False, show_variables=False): @@ -441,11 +448,9 @@ def finalize(self): proc.finalize() def clone(self): - """Clone the Model. + """Clone the Model, i.e., create a new Model instance with the same + process classes (but different instances). - This is equivalent to a deep copy, except that variable data - (i.e., `state`, `value`, `change` or `rate` properties) in all - processes are not copied. """ processes_cls = {k: type(obj) for k, obj in self._processes.items()} return type(self)(processes_cls) @@ -456,8 +461,8 @@ def update_processes(self, processes): Parameters ---------- processes : dict - Dictionnary with process names as keys and subclasses of - `Process` as values. + Dictionnary with process names as keys and process classes + as values. Returns ------- @@ -491,10 +496,8 @@ def drop_processes(self, keys): return type(self)(processes_cls) def __repr__(self): - n_inputs = sum([len(v) for v in self._input_vars.values()]) - hdr = ("" - % (len(self._processes), n_inputs)) + % (len(self._processes), len(self._input_vars))) if not len(self._processes): return hdr From 55ce03363861b21d00f66bb45fe64c5fd69d28f8 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 15:07:00 +0200 Subject: [PATCH 25/97] some docstring tweaks --- xsimlab/model.py | 64 +++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 52ef707b..6cd8a6cd 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -9,10 +9,15 @@ class _ModelBuilder(object): """Used to iteratively build a new model. - - Define variable keys in store - - Retrieve model inputs - - Reconstruct process dependencies and sort DAG of processes - - Split time dependent vs. independent processes + This builder implements the following tasks: + + - Assign a name for each process + - Define for each variable of the model its corresponding key + (in store or on-demand) + - Find variables that are model inputs + - Find process dependencies and sort processes (DAG) + - Find the processes that implement the method relative to each + step of a simulation """ def __init__(self, processes_cls): @@ -38,12 +43,13 @@ def _get_var_key(self, p_name, var): Returned key(s) are either None (if no key), a tuple or a list of tuples (for group variables). - A store key tuple looks like `('foo', 'bar')` where 'foo' is + A store key tuple looks like ``('foo', 'bar')`` where 'foo' is the name of any process in the model and 'bar' is the name of a variable declared in that process. - Similarly, an on-demand key tuple looks like `(foo_obj, 'bar')`, - but where `foo_obj` is a process object rather than its name. + Similarly, an on-demand key tuple looks like + ``(foo_obj, 'bar')``, but where ``foo_obj`` is a process object + rather than its name. """ store_key = None @@ -118,7 +124,7 @@ def set_process_keys(self): def get_input_variables(self): """Get all input variables in the model as a list of - `(process_name, var_name)` tuples. + ``(process_name, var_name)`` tuples. Model input variables meet the following conditions: @@ -151,12 +157,13 @@ def get_input_variables(self): def _maybe_add_dependency(self, p_name, p_obj, var_name, key): """Maybe add a process dependency based on single variable - `var_name`, defined in process `p_name`/`p_obj`, with the - corresponding `key` (either store or on-demand key). + ``var_name``, defined in process ``p_name``/``p_obj``, with + the corresponding ``key`` (either store or on-demand key). - A process depends on another process if it has a variable (or - foreign) for which the other process declares a foreign (or - variable) that provides a value (i.e., intent='out'). + A process depends on another process if it has a variable + (resp. a foreign) for which the other process declares a + foreign (resp. a variable) that provides a value (i.e., + intent='out'). """ if isinstance(key, list): @@ -293,8 +300,8 @@ def get_sorted_processes(self): return self._sorted_processes def get_time_processes(self): - """Time processes are process classes that implement `run_step` - and/or `finalize_step` method(s). + """Time processes are process classes that implement ``run_step`` + and/or ``finalize_step`` method(s). """ has_method = lambda obj, meth: callable(getattr(obj, meth, None)) @@ -353,7 +360,7 @@ def input_vars(self): """Returns all variables that require setting a value before running the model. - A list of `(process_name, var_name)` tuples (or an empty list) + A list of ``(process_name, var_name)`` tuples (or an empty list) is returned. """ @@ -364,8 +371,8 @@ def input_vars_dict(self): """Returns all variables that require setting a value before running the model. - Unlike `input_vars` property, a dictionary of lists of variable names - grouped by process is returned. + Unlike :attr:`Model.input_vars`, a dictionary of lists of + variable names grouped by process is returned. """ if self._input_vars_dict is None: @@ -378,12 +385,12 @@ def input_vars_dict(self): return self._input_vars_dict - def is_input(self, proc_name, var_name): + def is_input(self, process_name, var_name): """Test if a variable is an input of Model. Parameters ---------- - proc_name : str + process_name : str Name of a process. var_name : str Name of a variable declared in that process. @@ -392,10 +399,11 @@ def is_input(self, proc_name, var_name): ------- is_input : bool True if the variable is a input of Model (otherwise False, - even when `(proc_name, var_name)` doesn't exist in Model). + even when ``(process_name, var_name)`` doesn't refer to any + existing variable in Model). """ - return (proc_name, var_name) in self._input_vars + return (process_name, var_name) in self._input_vars def visualize(self, show_only_variable=None, show_inputs=False, show_variables=False): @@ -412,11 +420,11 @@ def visualize(self, show_only_variable=None, show_inputs=False, Ignored if `show_only_variable` is not None. show_variables : bool, optional If True, show also the other variables (default: False). - Ignored if `show_only_variable` is not None. + Ignored if ``show_only_variable`` is not None. See Also -------- - dot.dot_graph + :func:`dot.dot_graph` """ from .dot import dot_graph @@ -425,25 +433,25 @@ def visualize(self, show_only_variable=None, show_inputs=False, show_variables=show_variables) def initialize(self): - """Run `.initialize()` for each processes in the model.""" + """Run ``.initialize()`` for each processes in the model.""" for proc in self._processes.values(): proc.initialize() def run_step(self, step): - """Run `.run_step()` for each time dependent processes in the model. + """Run ``.run_step()`` for each time dependent processes in the model. """ for proc in self._time_processes.values(): proc.run_step(step) def finalize_step(self): - """Run `.finalize_step()` for each time dependent processes + """Run ``.finalize_step()`` for each time dependent processes in the model. """ for proc in self._time_processes.values(): proc.finalize_step() def finalize(self): - """Run `.finalize()` for each processes in the model.""" + """Run ``.finalize()`` for each processes in the model.""" for proc in self._processes.values(): proc.finalize() From 4b554f38e7453d44fb8da89c540739a103a84032 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 16:19:11 +0200 Subject: [PATCH 26/97] fix process deps when a variable involves more than 2 processes --- xsimlab/model.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 6cd8a6cd..11722f07 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -26,6 +26,8 @@ def __init__(self, processes_cls): self._reverse_lookup = {cls: k for k, cls in processes_cls.items()} + self._input_vars = None + self._dep_processes = None self._sorted_processes = None @@ -136,7 +138,7 @@ def get_input_variables(self): """ filter_in = lambda var: ( var.metadata['var_type'] != VarType.GROUP and - var.metadata['intent'] in (VarIntent.IN, VarIntent.INOUT) + var.metadata['intent'] != VarIntent.OUT ) filter_out = lambda var: var.metadata['intent'] == VarIntent.OUT @@ -153,17 +155,23 @@ def get_input_variables(self): for var in filter_variables(p_obj, func=filter_out).values() ] - return [k for k in set(in_keys) - set(out_keys) if k is not None] + self._input_vars = [k for k in set(in_keys) - set(out_keys) + if k is not None] + + return self._input_vars def _maybe_add_dependency(self, p_name, p_obj, var_name, key): """Maybe add a process dependency based on single variable ``var_name``, defined in process ``p_name``/``p_obj``, with the corresponding ``key`` (either store or on-demand key). - A process depends on another process if it has a variable - (resp. a foreign) for which the other process declares a - foreign (resp. a variable) that provides a value (i.e., - intent='out'). + Process 1 depends on process 2 if: + + - process 2 has a foreign variable with intent='out' targeting + a variable declared in process 1 ; + - process 1 has a foreign variable with intent!='out' targeting + a variable declared in process 2 that is not a model input (i.e., + process 2 or a 3rd process provides a value for that variable). """ if isinstance(key, list): @@ -177,13 +185,10 @@ def _maybe_add_dependency(self, p_name, p_obj, var_name, key): if not isinstance(target_p, str): # on-demand target variable target_p_name = self._reverse_lookup[type(target_p)] - target_p_obj = target_p else: target_p_name = target_p - target_p_obj = self._processes_obj[target_p_name] var = filter_variables(p_obj)[var_name] - target_var = filter_variables(target_p_obj)[target_var_name] if target_p_name == p_name: # not a foreign variable @@ -193,7 +198,7 @@ def _maybe_add_dependency(self, p_name, p_obj, var_name, key): # target process depends on current process self._dep_processes[target_p_name].add(p_name) - elif target_var.metadata['intent'] == VarIntent.OUT: + elif (target_p_name, target_var_name) not in self._input_vars: # current process depends on target process self._dep_processes[p_name].add(target_p_name) From 58404630c8a214655b1420d272898f4783d1ba37 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 17:12:05 +0200 Subject: [PATCH 27/97] identify which processes have to be executed at each simulation stage --- xsimlab/model.py | 54 ++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 11722f07..a1b11dfe 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -11,7 +11,7 @@ class _ModelBuilder(object): This builder implements the following tasks: - - Assign a name for each process + - Assign a given name for each process - Define for each variable of the model its corresponding key (in store or on-demand) - Find variables that are model inputs @@ -304,21 +304,16 @@ def get_sorted_processes(self): ) return self._sorted_processes - def get_time_processes(self): - """Time processes are process classes that implement ``run_step`` - and/or ``finalize_step`` method(s). + def get_stage_processes(self, stage): + """Return a (sorted) list of all process objects that implement the + method relative to a given simulation stage {'initialize', 'run_step', + 'finalize_step', 'finalize'}. """ has_method = lambda obj, meth: callable(getattr(obj, meth, None)) - is_time_process = lambda obj: (has_method(obj, 'run_step') or - has_method(obj, 'finalize_step')) - - return OrderedDict([ - (p_name, p_obj) - for p_name, p_obj in self._sorted_processes.items() - if is_time_process(p_obj) - ]) + return [p_obj for p_obj in self._sorted_processes.values() + if has_method(p_obj, stage)] class Model(AttrMapping, ContextMixin): @@ -355,7 +350,11 @@ def __init__(self, processes): self._dep_processes = builder.get_process_dependencies() self._processes = builder.get_sorted_processes() - self._time_processes = builder.get_time_processes() + + self._p_initialize = builder.get_stage_processes('initialize') + self._p_run_step = builder.get_stage_processes('run_step') + self._p_finalize_step = builder.get_stage_processes('finalize_step') + self._p_finalize = builder.get_stage_processes('finalize') super(Model, self).__init__(self._processes) self._initialized = True @@ -438,31 +437,28 @@ def visualize(self, show_only_variable=None, show_inputs=False, show_variables=show_variables) def initialize(self): - """Run ``.initialize()`` for each processes in the model.""" - for proc in self._processes.values(): - proc.initialize() + """Run the 'initialize' stage of a simulation.""" + for p in self._p_initialize: + p.initialize() def run_step(self, step): - """Run ``.run_step()`` for each time dependent processes in the model. - """ - for proc in self._time_processes.values(): - proc.run_step(step) + """Run a single 'run_step()' stage of a simulation.""" + for p in self._p_run_step: + p.run_step(step) def finalize_step(self): - """Run ``.finalize_step()`` for each time dependent processes - in the model. - """ - for proc in self._time_processes.values(): - proc.finalize_step() + """Run a single 'finalize_step' stage of a simulation.""" + for p in self._p_finalize_step: + p.finalize_step() def finalize(self): - """Run ``.finalize()`` for each processes in the model.""" - for proc in self._processes.values(): - proc.finalize() + """Run the 'finalize' stage of a simulation.""" + for p in self._p_finalize: + p.finalize() def clone(self): """Clone the Model, i.e., create a new Model instance with the same - process classes (but different instances). + process classes but different instances. """ processes_cls = {k: type(obj) for k, obj in self._processes.items()} From f9521cbec234c13d61da772333173c33881f22a7 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 29 Mar 2018 17:15:25 +0200 Subject: [PATCH 28/97] update whats new with some enhancements --- doc/whats_new.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index bc8f7353..2283769c 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -10,8 +10,8 @@ Highlights ~~~~~~~~~~ This release includes a major refactoring of both the internals and -the API on how processes and variables are defined and interact -between each other in a model. xarray-simlab now uses and extends +the API on how processes and variables are defined and depends on +each other in a model. xarray-simlab now uses and extends attrs_ (:issue:`33`). .. _attrs: http://www.attrs.org @@ -46,8 +46,8 @@ changes are effective now! properties ``state``, ``rate`` and ``change`` (e.g., ``self.foo.state``). Instead, all variables accept a unique value, which one can get/set by simply using the variable name (e.g., - ``self.foo``). You might want to create different variables to hold - different values. + ``self.foo``). Now multiple variables have to be declared for + holding different values. - Process classes are now defined using the ``process`` decorator instead of inheriting from a ``Process`` base class. @@ -68,6 +68,15 @@ changes are effective now! Enhancements ~~~~~~~~~~~~ +- The major refactoring in this release should reduce the overhead + caused by the indirect access to variable values in process objects. +- By creating read-only properties in specific cases (i.e., when + ``intent='in'``), the ``process`` decorator applied on a class adds + some safeguards to prevent setting variable values where it is not + intended. +- Some more sanity checks have been added when creating process + classes. + v0.1.1 (20 November 2017) ------------------------- From 4e698232877f6fbbc9f1698412bc4e7fd39af202 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 30 Mar 2018 19:11:16 +0200 Subject: [PATCH 29/97] add sanity checks for arg passed to Model constructor --- xsimlab/model.py | 22 +++++++++++++++++++++- xsimlab/process.py | 8 ++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index a1b11dfe..ade12ed8 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -1,7 +1,9 @@ from collections import OrderedDict, defaultdict +from inspect import isclass from .variable import VarIntent, VarType -from .process import filter_variables, get_target_variable +from .process import (filter_variables, get_target_variable, + NotAProcessClassError) from .utils import AttrMapping, ContextMixin from .formatting import _calculate_col_width, pretty_print, maybe_truncate @@ -339,7 +341,25 @@ def __init__(self, processes): Dictionnary with process names as keys and classes (decorated with :func:`process`) as values. + Raises + ------ + :exc:`TypeError` + If values in ``processes`` are not classes. + :exc:`NoteAProcessClassError` + If values in ``processes`` are not classes decorated with + :func:`process`. + """ + for cls in processes.values(): + if not isclass(cls): + raise TypeError("Dictionary values must be classes, " + "found {}".format(cls)) + + if not getattr(cls, "__xsimlab_process__", False): + raise NotAProcessClassError( + "{cls!r} is not a process-decorated class.".format(cls=cls) + ) + builder = _ModelBuilder(processes) builder.set_process_names() diff --git a/xsimlab/process.py b/xsimlab/process.py index d3fb45e3..b523efb2 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -4,6 +4,14 @@ from .variable import VarIntent, VarType +class NotAProcessClassError(ValueError): + """ + A non-``xsimlab.process`` class has been passed into a ``xsimlab`` + function. + + """ + pass + def filter_variables(process, var_type=None, intent=None, group=None, func=None): From acb70c3cdde4bc465a2b829ad19f2e858dc91fa1 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 30 Mar 2018 19:14:13 +0200 Subject: [PATCH 30/97] temp solution before attr 1.8 (attr.fields_dict) --- xsimlab/process.py | 7 ++++--- xsimlab/utils.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index b523efb2..502dd41d 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -3,6 +3,8 @@ import attr from .variable import VarIntent, VarType +from .utils import attr_fields_dict + class NotAProcessClassError(ValueError): """ @@ -40,10 +42,9 @@ def filter_variables(process, var_type=None, intent=None, group=None, """ if not isclass(process): - process = process.__class__ + process = type(process) - # TODO: use fields_dict instead (attrs 18.1.0) - fields = {a.name: a for a in attr.fields(process)} + fields = attr_fields_dict(process) if var_type is not None: fields = {k: a for k, a in fields.items() diff --git a/xsimlab/utils.py b/xsimlab/utils.py index f13d56a5..cfa86eeb 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -3,10 +3,26 @@ """ import threading -from collections import Mapping, KeysView, ItemsView, ValuesView +from collections import (Mapping, KeysView, ItemsView, ValuesView, + OrderedDict) from functools import wraps from contextlib import suppress from importlib import import_module +from inspect import isclass + +import attr + + +def attr_fields_dict(cls): + # TODO: remove this and use attr.fields_dict instead (18.1.0) + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise attr.NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return OrderedDict(((a.name, a) for a in attrs)) def import_required(mod_name, error_msg): From c5a3933848188ee8cd9d831c50f620c449423121 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 30 Mar 2018 19:16:26 +0200 Subject: [PATCH 31/97] update Model repr (and move code to formatting module) --- xsimlab/formatting.py | 44 +++++++++++++++++++++++++++++++++++++++++++ xsimlab/model.py | 30 ++--------------------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 028be365..7cc91e0b 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -1,4 +1,5 @@ """Formatting utils and functions.""" +from .utils import attr_fields_dict def _calculate_col_width(col_items): @@ -78,3 +79,46 @@ def process_info(cls_or_obj): ) return '\n'.join([var_block, meta_block]) +def repr_model(model): + n_processes = len(model) + + hdr = ("" + .format(n_processes, len(model.input_vars))) + + if not n_processes: + return hdr + + max_line_length = 70 + col_width = max([_calculate_col_width(var_name) + for var_name in model.input_vars]) + + sections = [] + + for p_name, p_obj in model.items(): + p_section = p_name + + p_input_vars = model.input_vars_dict.get(p_name, []) + input_var_lines = [] + + for var_name in p_input_vars: + var = attr_fields_dict(type(p_obj))[var_name] + rcol_items = [] + + var_dims = " or ".join([str(d) for d in var.metadata['dims']]) + if var_dims != "()": + rcol_items.append(var_dims) + + rcol_items += ["[{}]".format(var.metadata['intent'].value), + var.metadata['description']] + + line = pretty_print(" {} ".format(var_name), col_width) + line += maybe_truncate(' '.join(rcol_items), + max_line_length - col_width) + + input_var_lines.append(line) + + if input_var_lines: + p_section += '\n' + '\n'.join(input_var_lines) + sections.append(p_section) + + return hdr + '\n' + '\n'.join(sections) diff --git a/xsimlab/model.py b/xsimlab/model.py index ade12ed8..f44e66d3 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -5,7 +5,7 @@ from .process import (filter_variables, get_target_variable, NotAProcessClassError) from .utils import AttrMapping, ContextMixin -from .formatting import _calculate_col_width, pretty_print, maybe_truncate +from .formatting import repr_model class _ModelBuilder(object): @@ -525,30 +525,4 @@ def drop_processes(self, keys): return type(self)(processes_cls) def __repr__(self): - hdr = ("" - % (len(self._processes), len(self._input_vars))) - - if not len(self._processes): - return hdr - - max_line_length = 70 - col_width = max([_calculate_col_width(var) - for var in self._input_vars.values()]) - - blocks = [] - for proc_name in self._processes: - proc_str = "%s" % proc_name - - inputs = self._input_vars.get(proc_name, {}) - lines = [] - for name, var in inputs.items(): - line = pretty_print(" %s " % name, col_width) - line += maybe_truncate("(in) %s" % var.description, - max_line_length - col_width) - lines.append(line) - - if lines: - proc_str += '\n' + '\n'.join(lines) - blocks.append(proc_str) - - return hdr + '\n' + '\n'.join(blocks) + return repr_model(self) From 1de6f6227ac697789b23ef6ca7348d5ee31c3f02 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 30 Mar 2018 19:17:31 +0200 Subject: [PATCH 32/97] (wip) update process repr --- xsimlab/formatting.py | 22 ++++++++++++++++++++++ xsimlab/process.py | 7 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 7cc91e0b..879806f8 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -79,6 +79,28 @@ def process_info(cls_or_obj): ) return '\n'.join([var_block, meta_block]) + + +def repr_process(process): + process_cls = type(process) + + hdr = "<{} (xsimlab process)>".format(process_cls.__name__) + + variables = attr_fields_dict(process_cls) + + col_width = _calculate_col_width([k for k in variables]) + max_line_length = 70 + + var_section = "Variables:\n" + + # TODO: if __xsimlab_name__ is set and not None, + # add process name in header + # TODO: complete repr with variable list and possibly + # simulation stages implemented + + return hdr + + def repr_model(model): n_processes = len(model) diff --git a/xsimlab/process.py b/xsimlab/process.py index 502dd41d..0d114509 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -257,7 +257,7 @@ class _ProcessBuilder(object): def __init__(self, attr_cls): self._cls = attr_cls - self._cls_dict = {} + self._cls_dict = {'__xsimlab_process__': True} def add_properties(self, var_type): make_prop_func = self._make_prop_funcs[var_type] @@ -265,6 +265,9 @@ def add_properties(self, var_type): for var_name, var in filter_variables(self._cls, var_type).items(): self._cls_dict[var_name] = make_prop_func(var) + def add_repr(self): + self._cls_dict['__repr__'] = repr_process + def render_docstrings(self): # self._cls_dict['__doc__'] = "Process-ified class." raise NotImplementedError("autodoc is not yet implemented.") @@ -322,6 +325,8 @@ def wrap(cls): if autodoc: builder.render_docstrings() + builder.add_repr() + return builder.build_class() if maybe_cls is None: From 1e06da3a702fd4d43c833e12f8b98e2c239c92bf Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 30 Mar 2018 19:18:12 +0200 Subject: [PATCH 33/97] (wip) add process_info and variable_info helper functions --- xsimlab/__init__.py | 5 ++--- xsimlab/process.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/xsimlab/__init__.py b/xsimlab/__init__.py index 5d837ef1..0d9df25c 100644 --- a/xsimlab/__init__.py +++ b/xsimlab/__init__.py @@ -2,11 +2,10 @@ xarray-simlab. """ -from .xr_accessor import SimlabAccessor, create_setup +#from .xr_accessor import SimlabAccessor, create_setup from .variable import variable, on_demand, foreign, group -from .process import get_variables, process +from .process import filter_variables, process, process_info, variable_info from .model import Model -from .process import filter_variables, process from ._version import get_versions versions = get_versions() diff --git a/xsimlab/process.py b/xsimlab/process.py index 0d114509..0bcc817b 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -3,6 +3,7 @@ import attr from .variable import VarIntent, VarType +from .formatting import repr_process from .utils import attr_fields_dict @@ -333,3 +334,22 @@ def wrap(cls): return wrap else: return wrap(maybe_cls) + + +def process_info(process): + """Equivalent to __repr__ of a process but accepts + either an instance or a class. + + """ + # TODO: + raise NotImplementedError() + + +def variable_info(process, var_name): + """Get more information about a variable (all metadata). + + ``process`` is either a class or an instance. + + """ + # TODO: + raise NotImplementedError() From bab4e3903c9cb092892e73aefe2d0e476da7ed31 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sat, 31 Mar 2018 01:09:35 +0200 Subject: [PATCH 34/97] bind model instance to processes + simplify on-demand keys --- xsimlab/model.py | 30 ++++++++++-------------------- xsimlab/process.py | 20 +++++++++++--------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index f44e66d3..e377af76 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -13,7 +13,8 @@ class _ModelBuilder(object): This builder implements the following tasks: - - Assign a given name for each process + - Attach the model instance to each process and assign their given + name in model. - Define for each variable of the model its corresponding key (in store or on-demand) - Find variables that are model inputs @@ -36,8 +37,9 @@ def __init__(self, processes_cls): # a cache for group keys self._group_keys = {} - def set_process_names(self): + def bind_processes(self, model_obj): for p_name, p_obj in self._processes_obj.items(): + p_obj.__xsimlab_model__ = model_obj p_obj.__xsimlab_name__ = p_name def _get_var_key(self, p_name, var): @@ -47,13 +49,9 @@ def _get_var_key(self, p_name, var): Returned key(s) are either None (if no key), a tuple or a list of tuples (for group variables). - A store key tuple looks like ``('foo', 'bar')`` where 'foo' is - the name of any process in the model and 'bar' is the name of - a variable declared in that process. - - Similarly, an on-demand key tuple looks like - ``(foo_obj, 'bar')``, but where ``foo_obj`` is a process object - rather than its name. + A key tuple looks like ``('foo', 'bar')`` where 'foo' is the + name of any process in the model and 'bar' is the name of a + variable declared in that process. """ store_key = None @@ -66,12 +64,10 @@ def _get_var_key(self, p_name, var): elif var_type == VarType.FOREIGN: target_p_cls, target_var = get_target_variable(var) - target_p_name = self._reverse_lookup[target_p_cls] - target_p_obj = self._processes_obj[target_p_name] if target_var.metadata['var_type'] == VarType.ON_DEMAND: - od_key = (target_p_obj, target_var.name) + od_key = (target_p_name, target_var.name) else: store_key = (target_p_name, target_var.name) @@ -182,13 +178,7 @@ def _maybe_add_dependency(self, p_name, p_obj, var_name, key): self._maybe_add_dependency(p_name, p_obj, var_name, k) else: - target_p, target_var_name = key - - if not isinstance(target_p, str): - # on-demand target variable - target_p_name = self._reverse_lookup[type(target_p)] - else: - target_p_name = target_p + target_p_name, target_var_name = key var = filter_variables(p_obj)[var_name] @@ -362,7 +352,7 @@ def __init__(self, processes): builder = _ModelBuilder(processes) - builder.set_process_names() + builder.bind_processes(self) builder.set_process_keys() self._input_vars = builder.get_input_variables() diff --git a/xsimlab/process.py b/xsimlab/process.py index 0bcc817b..b7e4a753 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -110,11 +110,13 @@ def _attrify_class(cls): into :class:`attr.Attribute` objects and it also adds dunder-methods such as `__init__`. - The following instance attributes are also defined (values will be - set later at model creation): + The following instance attributes are also defined with None or + empty values (proper values will be set later at model creation): + __xsimlab_model__ : obj + :class:`Model` instance to which the process instance is attached. __xsimlab_name__ : str - Name given for this process in a model. + Name given for this process in the model. __xsimlab_store__ : dict or object Simulation data store. __xsimlab_store_keys__ : dict @@ -126,12 +128,11 @@ def _attrify_class(cls): __xsimlab_od_keys__ : dict Dictionary that maps variable names to the location of their target on-demand variable (or a list of locations for group variables). - Location here consists of pairs like `(foo_obj, 'bar')`, where - `foo_obj` is any process in the same model 'bar' is the name of a - variable declared in that process. + Locations are tuples like store keys. """ def init_process(self): + self.__xsimlab_model__ = None self.__xsimlab_name__ = None self.__xsimlab_store__ = None self.__xsimlab_store_keys__ = {} @@ -160,8 +161,9 @@ def get_from_store(self): return self.__xsimlab_store__[key] def get_on_demand(self): - od_key = self.__xsimlab_od_keys__[var_name] - return getattr(*od_key) + p_name, v_name = self.__xsimlab_od_keys__[var_name] + p_obj = self.__xsimlab_model__._processes[p_name] + return getattr(p_obj, v_name) def put_in_store(self, value): key = self.__xsimlab_store_keys__[var_name] @@ -193,7 +195,7 @@ def put_in_store(self, value): target_str, target_intent.value)) elif target_type == VarType.ON_DEMAND: - if var_intent in (VarIntent.OUT, VarIntent.INOUT): + if var_intent != VarIntent.IN: raise ValueError("Variable '{}' targeting on-demand variable " "'{}' should have intent='in' (found '{}')" .format(var.name, target_str, var_intent.value)) From c278aa6b1f51839d37c57b53a63248bc7ee757c3 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sat, 31 Mar 2018 01:54:32 +0200 Subject: [PATCH 35/97] better error msg when a dependent process is missing in model --- xsimlab/model.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index e377af76..f98e755c 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -64,7 +64,14 @@ def _get_var_key(self, p_name, var): elif var_type == VarType.FOREIGN: target_p_cls, target_var = get_target_variable(var) - target_p_name = self._reverse_lookup[target_p_cls] + target_p_name = self._reverse_lookup.get(target_p_cls, None) + + if target_p_name is None: + raise KeyError( + "Process class '{}' missing in Model but required " + "by foreign variable '{}' declared in process '{}'" + .format(target_p_cls.__name__, var.name, p_name) + ) if target_var.metadata['var_type'] == VarType.ON_DEMAND: od_key = (target_p_name, target_var.name) From 82893518ef0719d20fa77739b0ac5c9e8d6915a1 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 10:46:23 +0200 Subject: [PATCH 36/97] update process repr and variable inline summary --- xsimlab/formatting.py | 133 +++++++++++++++++++++--------------------- xsimlab/model.py | 15 +---- xsimlab/process.py | 37 ++++++++++-- xsimlab/utils.py | 4 ++ 4 files changed, 106 insertions(+), 83 deletions(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 879806f8..9d4eb796 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -1,5 +1,6 @@ """Formatting utils and functions.""" -from .utils import attr_fields_dict +from .utils import attr_fields_dict, has_method +from .variable import VarIntent, VarType def _calculate_col_width(col_items): @@ -31,88 +32,100 @@ def wrap_indent(text, start='', length=None): return start + indent.join(x for x in text.splitlines()) -def _summarize_var(name, var, col_width, marker=' '): +def _summarize_var(var, process, col_width): max_line_length = 70 - first_col = pretty_print(" %s %s" % (marker, name), col_width) + var_name = var.name + var_type = var.metadata['var_type'] + var_intent = var.metadata['intent'] - if isinstance(var, tuple): - var_repr = "VariableList" - else: - var_repr = str(var).strip('<>').replace('xsimlab.', '') - var_repr = maybe_truncate(var_repr, max_line_length - col_width) - - return first_col + var_repr + if var_intent == VarIntent.IN: + link_symbol = '<---' + elif var_intent == VarIntent.OUT: + link_symbol = '--->' + if var_type == VarType.GROUP: + var_info = '{} group {!r}'.format(link_symbol, var.metadata['group']) -def _summarize_var_list(name, var, col_width): - vars_lines = '\n'.join([_summarize_var('- ', v, col_width) - for v in var]) - return '\n'.join([_summarize_var(name, var, col_width), vars_lines]) + elif var_type == VarType.FOREIGN: + key = process.__xsimlab_store_keys__.get(var_name) + if key is None: + key = process.__xsimlab_od_keys__.get(var_name) + if key is None: + key = (var.metadata['other_process_cls'].__name__, + var.metadata['var_name']) + var_info = '{} {}.{}'.format(link_symbol, *key) -def process_info(cls_or_obj): - col_width = _calculate_col_width(cls_or_obj._variables) - max_line_length = 70 - - var_block = "Variables:\n" + else: + var_dims = " or ".join([str(d) for d in var.metadata['dims']]) - lines = [] - for name, var in cls_or_obj._variables.items(): - if isinstance(var, (tuple, list)): - line = _summarize_var_list(name, var, col_width) + if var_dims != "()": + var_info = " ".join([var_dims, var.metadata['description']]) else: - marker = '*' if var.provided else ' ' - line = _summarize_var(name, var, col_width, - marker=marker) - lines.append(line) + var_info = var.metadata['description'] - if not lines: - var_block += " *empty*" - else: - var_block += '\n'.join(lines) + left_col = pretty_print(" {}".format(var.name), col_width) - meta_block = "Meta:\n" - meta_block += '\n'.join( - [maybe_truncate(" %s: %s" % (k, v), max_line_length) - for k, v in cls_or_obj._meta.items()] + right_col = maybe_truncate( + "[{}] {}".format(var_intent.value, var_info), + max_line_length - col_width ) - return '\n'.join([var_block, meta_block]) + return left_col + right_col def repr_process(process): process_cls = type(process) - hdr = "<{} (xsimlab process)>".format(process_cls.__name__) + if process.__xsimlab_name__ is not None: + process_name = '{!r}'.format(process.__xsimlab_name__) + else: + process_name = '' + + header = "<{} {} (xsimlab process)>".format(process_cls.__name__, + process_name) variables = attr_fields_dict(process_cls) - col_width = _calculate_col_width([k for k in variables]) - max_line_length = 70 + col_width = _calculate_col_width(variables) - var_section = "Variables:\n" + var_section_summary = "Variables:" + var_section_details = "\n".join( + [_summarize_var(var, process, col_width) for var in variables.values()] + ) - # TODO: if __xsimlab_name__ is set and not None, - # add process name in header - # TODO: complete repr with variable list and possibly - # simulation stages implemented + stages_implemented = [ + " {}".format(m) + for m in ['initialize', 'run_step', 'finalize_step', 'finalize'] + if has_method(process, m) + ] - return hdr + stages_section_summary = "Simulation stages:" + if stages_implemented: + stages_section_details = "\n".join(stages_implemented) + else: + stages_section_details = " *no stage implemented*" + + return "\n".join([header, + var_section_summary, + var_section_details, + stages_section_summary, + stages_section_details]) def repr_model(model): n_processes = len(model) - hdr = ("" - .format(n_processes, len(model.input_vars))) + header = ("" + .format(n_processes, len(model.input_vars))) if not n_processes: - return hdr + return header - max_line_length = 70 - col_width = max([_calculate_col_width(var_name) - for var_name in model.input_vars]) + col_width = _calculate_col_width( + [var_name for _, var_name in model.input_vars] + ) sections = [] @@ -124,23 +137,11 @@ def repr_model(model): for var_name in p_input_vars: var = attr_fields_dict(type(p_obj))[var_name] - rcol_items = [] - - var_dims = " or ".join([str(d) for d in var.metadata['dims']]) - if var_dims != "()": - rcol_items.append(var_dims) - - rcol_items += ["[{}]".format(var.metadata['intent'].value), - var.metadata['description']] - - line = pretty_print(" {} ".format(var_name), col_width) - line += maybe_truncate(' '.join(rcol_items), - max_line_length - col_width) - - input_var_lines.append(line) + input_var_lines.append(_summarize_var(var, p_obj, col_width)) if input_var_lines: p_section += '\n' + '\n'.join(input_var_lines) + sections.append(p_section) - return hdr + '\n' + '\n'.join(sections) + return header + '\n' + '\n'.join(sections) diff --git a/xsimlab/model.py b/xsimlab/model.py index f98e755c..40c3e1c6 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -2,9 +2,8 @@ from inspect import isclass from .variable import VarIntent, VarType -from .process import (filter_variables, get_target_variable, - NotAProcessClassError) -from .utils import AttrMapping, ContextMixin +from .process import ensure_process, filter_variables, get_target_variable +from .utils import AttrMapping, ContextMixin, has_method from .formatting import repr_model @@ -186,7 +185,6 @@ def _maybe_add_dependency(self, p_name, p_obj, var_name, key): else: target_p_name, target_var_name = key - var = filter_variables(p_obj)[var_name] if target_p_name == p_name: @@ -210,7 +208,6 @@ def get_process_dependencies(self): self._dep_processes = {k: set() for k in self._processes_obj} for p_name, p_obj in self._processes_obj.items(): - store_keys = p_obj.__xsimlab_store_keys__ od_keys = p_obj.__xsimlab_od_keys__ @@ -309,8 +306,6 @@ def get_stage_processes(self, stage): 'finalize_step', 'finalize'}. """ - has_method = lambda obj, meth: callable(getattr(obj, meth, None)) - return [p_obj for p_obj in self._sorted_processes.values() if has_method(p_obj, stage)] @@ -351,11 +346,7 @@ def __init__(self, processes): if not isclass(cls): raise TypeError("Dictionary values must be classes, " "found {}".format(cls)) - - if not getattr(cls, "__xsimlab_process__", False): - raise NotAProcessClassError( - "{cls!r} is not a process-decorated class.".format(cls=cls) - ) + ensure_process(cls) builder = _ModelBuilder(processes) diff --git a/xsimlab/process.py b/xsimlab/process.py index b7e4a753..13ae6d49 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -1,4 +1,5 @@ from inspect import isclass +import sys import attr @@ -16,6 +17,13 @@ class NotAProcessClassError(ValueError): pass +def ensure_process(cls): + if not getattr(cls, "__xsimlab_process__", False): + raise NotAProcessClassError( + "{cls!r} is not a process-decorated class.".format(cls=cls) + ) + + def filter_variables(process, var_type=None, intent=None, group=None, func=None): """Filter the variables declared in a process. @@ -176,6 +184,9 @@ def put_in_store(self, value): var_intent = var.metadata['intent'] target_intent = target_var.metadata['intent'] + # TODO: add var description (or possibly more like output of variable_info) + # in properties docstrings + if target_process_cls is not None: target_str = '.'.join([target_process_cls.__name__, target_var.name]) else: @@ -338,13 +349,29 @@ def wrap(cls): return wrap(maybe_cls) -def process_info(process): - """Equivalent to __repr__ of a process but accepts - either an instance or a class. +def process_info(process, buf=None): + """Concise summary of process variables and simulation stages + implemented. + + Equivalent to __repr__ of a process but accepts either an instance + or a class. + + Parameters + ---------- + process : object or class + Process class or object. + buf : object, optional + Writable buffer (default: sys.stdout). """ - # TODO: - raise NotImplementedError() + if isclass(process): + ensure_process(process) + process = process() + + if buf is None: # pragma: no cover + buf = sys.stdout + + buf.write(repr_process(process)) def variable_info(process, var_name): diff --git a/xsimlab/utils.py b/xsimlab/utils.py index cfa86eeb..b2acb30b 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -25,6 +25,10 @@ def attr_fields_dict(cls): return OrderedDict(((a.name, a) for a in attrs)) +def has_method(obj, meth): + return callable(getattr(obj, meth, False)) + + def import_required(mod_name, error_msg): """Attempt to import a required dependency. Raises a RuntimeError if the requested module is not available. From df732e46ba8f2d727bbe0222aabdfc6c8cd4ed9a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 11:47:59 +0200 Subject: [PATCH 37/97] update whats new --- doc/whats_new.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 2283769c..cc5ecd89 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -70,6 +70,10 @@ Enhancements - The major refactoring in this release should reduce the overhead caused by the indirect access to variable values in process objects. +- Another benefit of the refactoring is that a process-decorated class + may now inherit from other classes (possibly also + process-decorated), which allows more flexibility in model + customization. - By creating read-only properties in specific cases (i.e., when ``intent='in'``), the ``process`` decorator applied on a class adds some safeguards to prevent setting variable values where it is not From 26aca421670f5565b7c7f309c7e44ccdc1ba721c Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 11:51:07 +0200 Subject: [PATCH 38/97] remove Model.is_input --- doc/api.rst | 1 - doc/whats_new.rst | 9 ++++----- xsimlab/model.py | 20 -------------------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 26532ce8..1d85f61c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -86,7 +86,6 @@ access is also supported). :toctree: _api_generated/ Model.input_vars - Model.is_input Model.visualize Running a model diff --git a/doc/whats_new.rst b/doc/whats_new.rst index cc5ecd89..37966ec6 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -57,13 +57,12 @@ changes are effective now! - Using ``class Meta`` inside a process class to define some metadata is not used anymore. -- ``Model.is_input`` no longer accepts a Variable object as - argument. Instead, it accepts two arguments (process name and - variable name). - ``Model.input_vars`` now returns a list of ``(process_name, - variable_name)`` tuples instead of a dict of - dicts. ``Model.input_vars_dict`` has been added for convenience + variable_name)`` tuples instead of a dict of dicts. + ``Model.input_vars_dict`` has been added for convenience (i.e., to get input variables grouped by process as a dictionary). +- ``Model.is_input`` has been removed. Use ``Model.input_vars`` + instead to check if a variable is a model input. Enhancements ~~~~~~~~~~~~ diff --git a/xsimlab/model.py b/xsimlab/model.py index 40c3e1c6..d8b7ec70 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -397,26 +397,6 @@ def input_vars_dict(self): return self._input_vars_dict - def is_input(self, process_name, var_name): - """Test if a variable is an input of Model. - - Parameters - ---------- - process_name : str - Name of a process. - var_name : str - Name of a variable declared in that process. - - Returns - ------- - is_input : bool - True if the variable is a input of Model (otherwise False, - even when ``(process_name, var_name)`` doesn't refer to any - existing variable in Model). - - """ - return (process_name, var_name) in self._input_vars - def visualize(self, show_only_variable=None, show_inputs=False, show_variables=False): """Render the model as a graph using dot (require graphviz). From 8bddcad4d4370065fa3c3483080f2fc27ef6b97e Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 15:10:33 +0200 Subject: [PATCH 39/97] show variable details --- xsimlab/formatting.py | 19 ++++++++++++ xsimlab/model.py | 5 ++-- xsimlab/process.py | 67 ++++++++++++++++++++++++++++++++----------- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 9d4eb796..cb9809f7 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -1,4 +1,6 @@ """Formatting utils and functions.""" +import textwrap + from .utils import attr_fields_dict, has_method from .variable import VarIntent, VarType @@ -75,6 +77,23 @@ def _summarize_var(var, process, col_width): return left_col + right_col +def var_details(var): + max_line_length = 70 + + var_metadata = var.metadata.copy() + + description = textwrap.fill(var_metadata.pop('description'), + width=max_line_length) + + detail_items = [('type', var_metadata.pop('var_type').value), + ('intent', var_metadata.pop('intent').value)] + detail_items += list(var_metadata.items()) + + details = "\n".join(["- {} : {}".format(k, v) for k, v in detail_items]) + + return description + "\n\n" + details + + def repr_process(process): process_cls = type(process) diff --git a/xsimlab/model.py b/xsimlab/model.py index d8b7ec70..fa545d51 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -2,7 +2,8 @@ from inspect import isclass from .variable import VarIntent, VarType -from .process import ensure_process, filter_variables, get_target_variable +from .process import (ensure_process_decorated, filter_variables, + get_target_variable) from .utils import AttrMapping, ContextMixin, has_method from .formatting import repr_model @@ -346,7 +347,7 @@ def __init__(self, processes): if not isclass(cls): raise TypeError("Dictionary values must be classes, " "found {}".format(cls)) - ensure_process(cls) + ensure_process_decorated(cls) builder = _ModelBuilder(processes) diff --git a/xsimlab/process.py b/xsimlab/process.py index 13ae6d49..f6884990 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -4,7 +4,7 @@ import attr from .variable import VarIntent, VarType -from .formatting import repr_process +from .formatting import repr_process, var_details from .utils import attr_fields_dict @@ -17,11 +17,34 @@ class NotAProcessClassError(ValueError): pass -def ensure_process(cls): +def ensure_process_decorated(cls): if not getattr(cls, "__xsimlab_process__", False): - raise NotAProcessClassError( - "{cls!r} is not a process-decorated class.".format(cls=cls) - ) + raise NotAProcessClassError("{cls!r} is not a " + "process-decorated class.".format(cls=cls)) + + +def get_process_cls(obj_or_cls): + if not isclass(obj_or_cls): + cls = type(obj_or_cls) + else: + cls = obj_or_cls + + ensure_process_decorated(cls) + + return cls + + +def get_process_obj(obj_or_cls): + if isclass(obj_or_cls): + cls = obj_or_cls + obj = cls() + else: + cls = type(obj_or_cls) + obj = obj_or_cls + + ensure_process_decorated(cls) + + return obj def filter_variables(process, var_type=None, intent=None, group=None, @@ -50,8 +73,7 @@ def filter_variables(process, var_type=None, intent=None, group=None, objects as values. """ - if not isclass(process): - process = type(process) + process = get_process_cls(process) fields = attr_fields_dict(process) @@ -271,7 +293,8 @@ class _ProcessBuilder(object): def __init__(self, attr_cls): self._cls = attr_cls - self._cls_dict = {'__xsimlab_process__': True} + self._cls.__xsimlab_process__ = True + self._cls_dict = {} def add_properties(self, var_type): make_prop_func = self._make_prop_funcs[var_type] @@ -364,21 +387,31 @@ def process_info(process, buf=None): Writable buffer (default: sys.stdout). """ - if isclass(process): - ensure_process(process) - process = process() - if buf is None: # pragma: no cover buf = sys.stdout + process = get_process_obj(process) + buf.write(repr_process(process)) -def variable_info(process, var_name): - """Get more information about a variable (all metadata). +def variable_info(process, var_name, buf=None): + """Get detailed information about a variable. - ``process`` is either a class or an instance. + Parameters + ---------- + process : object or class + Process class or object. + var_name : str + Variable name. + buf : object, optional + Writable buffer (default: sys.stdout). """ - # TODO: - raise NotImplementedError() + if buf is None: # pragma: no cover + buf = sys.stdout + + process = get_process_cls(process) + var = attr_fields_dict(process)[var_name] + + buf.write(var_details(var)) From fe0143239e5818601bb09ee201c4bc441321be9b Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 15:36:52 +0200 Subject: [PATCH 40/97] add description to foreign and group variables --- xsimlab/variable.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index ff349aa6..ee492489 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -213,10 +213,15 @@ def foreign(other_process_cls, var_name, intent='in'): :func:`variable` """ + description = ("Reference to variable {!r} " + "defined in class {!r}" + .format(var_name, other_process_cls.__name__)) + metadata = {'var_type': VarType.FOREIGN, 'other_process_cls': other_process_cls, 'var_name': var_name, - 'intent': VarIntent(intent)} + 'intent': VarIntent(intent), + 'description': description} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) @@ -241,8 +246,12 @@ def group(name): :func:`variable` """ + description = ("Iterable of all variables that " + "belong to group {!r}".format(name)) + metadata = {'var_type': VarType.GROUP, 'group': name, - 'intent': VarIntent.IN} + 'intent': VarIntent.IN, + 'description': description} return attr.attrib(metadata=metadata, init=False, cmp=False, repr=False) From 5f66f3480798fd04677ed6fdf7b0dea4094a38fb Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 15:37:35 +0200 Subject: [PATCH 41/97] add docstrings for variable-related properties in process classes --- xsimlab/formatting.py | 2 +- xsimlab/process.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index cb9809f7..8af82af4 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -82,7 +82,7 @@ def var_details(var): var_metadata = var.metadata.copy() - description = textwrap.fill(var_metadata.pop('description'), + description = textwrap.fill(var_metadata.pop('description').capitalize(), width=max_line_length) detail_items = [('type', var_metadata.pop('var_type').value), diff --git a/xsimlab/process.py b/xsimlab/process.py index f6884990..89f62a4a 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -206,8 +206,7 @@ def put_in_store(self, value): var_intent = var.metadata['intent'] target_intent = target_var.metadata['intent'] - # TODO: add var description (or possibly more like output of variable_info) - # in properties docstrings + var_doc = var_details(var) if target_process_cls is not None: target_str = '.'.join([target_process_cls.__name__, target_var.name]) @@ -233,13 +232,13 @@ def put_in_store(self, value): "'{}' should have intent='in' (found '{}')" .format(var.name, target_str, var_intent.value)) - return property(fget=get_on_demand) + return property(fget=get_on_demand, doc=var_doc) elif var_type == VarIntent.IN: - return property(fget=get_from_store) + return property(fget=get_from_store, doc=var_doc) else: - return property(fget=get_from_store, fset=put_in_store) + return property(fget=get_from_store, fset=put_in_store, doc=var_doc) def _make_property_on_demand(var): @@ -256,7 +255,7 @@ def _make_property_on_demand(var): get_method = var.metadata['compute'] - return property(fget=get_method) + return property(fget=get_method, doc=var_details(var)) def _make_property_group(var): @@ -274,7 +273,7 @@ def getter_store_or_on_demand(self): for key in od_keys: yield getattr(*key) - return property(fget=getter_store_or_on_demand) + return property(fget=getter_store_or_on_demand, doc=var_details(var)) class _ProcessBuilder(object): From 05c5b63e04ef1f16fcb83407b67e241917a8f775 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 15:48:31 +0200 Subject: [PATCH 42/97] update what's new --- doc/whats_new.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 37966ec6..7021fc46 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -64,6 +64,12 @@ changes are effective now! - ``Model.is_input`` has been removed. Use ``Model.input_vars`` instead to check if a variable is a model input. +- ``__repr__`` has slightly changed for variables, processes and + models. Process classes don't have an ``.info()`` method anymore, + which has been replaced by the ``process_info()`` top-level + function. Another helper function ``variable_info()`` has been + added. + Enhancements ~~~~~~~~~~~~ From baeebe626e3dcad2253b8b5b59af3ecba9801ce2 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 16:14:34 +0200 Subject: [PATCH 43/97] add Model.dependent_processes property --- doc/whats_new.rst | 2 ++ xsimlab/model.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 7021fc46..c38637ef 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -85,6 +85,8 @@ Enhancements intended. - Some more sanity checks have been added when creating process classes. +- Added ``Model.dependent_processes`` property (so far this was not + public API). v0.1.1 (20 November 2017) ------------------------- diff --git a/xsimlab/model.py b/xsimlab/model.py index fa545d51..51c8c6c9 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -398,6 +398,14 @@ def input_vars_dict(self): return self._input_vars_dict + @property + def dependent_processes(self): + """Returns a dictionary where keys are process names and values are + lists of the names of dependent processes. + + """ + return self._dep_processes + def visualize(self, show_only_variable=None, show_inputs=False, show_variables=False): """Render the model as a graph using dot (require graphviz). From 10041621353fde2f9b137295f4fa2663be067581 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 5 Apr 2018 20:18:06 +0200 Subject: [PATCH 44/97] update model visualization (dot) --- doc/whats_new.rst | 4 ++ xsimlab/dot.py | 136 ++++++++++++++++++++++------------------------ xsimlab/model.py | 8 +-- xsimlab/utils.py | 4 ++ 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index c38637ef..27178dcc 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -70,6 +70,10 @@ changes are effective now! function. Another helper function ``variable_info()`` has been added. +- In ``Model.visualize()`` and ``xsimlab.dot.dot_graph()``, + ``show_variables=True`` now shows all model variables including + inputs. + Enhancements ~~~~~~~~~~~~ diff --git a/xsimlab/dot.py b/xsimlab/dot.py index 44ec6cf3..1dd71670 100644 --- a/xsimlab/dot.py +++ b/xsimlab/dot.py @@ -9,14 +9,11 @@ http://dask.pydata.org """ -from __future__ import absolute_import, division, print_function - import os from functools import partial -from .utils import import_required -from .variable.base import (AbstractVariable, ForeignVariable, - DiagnosticVariable, VariableGroup) +from .utils import attr_fields_dict, import_required, maybe_to_list +from .variable import VarIntent, VarType graphviz = import_required("graphviz", "Drawing dask graphs requires the " @@ -36,91 +33,91 @@ def hash_variable(var): - return str(hash(var)) + # issue with variables with the same name declared in different processes + # return str(hash(var)) + return str(id(var)) def _add_processes(g, model): seen = set() - for proc_name, proc in model._processes.items(): - label = proc_name - if proc_name not in seen: - seen.add(proc_name) - g.node(proc_name, label=label, **PROC_NODE_ATTRS) + for p_name, p_obj in model._processes.items(): + if p_name not in seen: + seen.add(p_name) + g.node(p_name, label=p_name, **PROC_NODE_ATTRS) - for dep_proc_name in model._dep_processes[proc_name]: - # check and add node shouldn't be needed here, but not sure - #if dep_proc_name not in seen: - # seen.add(dep_proc_name) - # dep_label = dep_proc_name - # g.node(dep_proc_name, label=dep_label, **PROC_NODE_ATTRS) - g.edge(dep_proc_name, proc_name, **PROC_EDGE_ATTRS) + for dep_p_name in model.dependent_processes[p_name]: + g.edge(dep_p_name, p_name, **PROC_EDGE_ATTRS) -def _add_var(g, var, label, link_2_node, is_input=False): +def _add_var(g, model, var, p_name): node_attrs = VAR_NODE_ATTRS.copy() edge_attrs = VAR_EDGE_ATTRS.copy() + var_key = hash_variable(var) + var_intent = var.metadata['intent'] + var_type = var.metadata['var_type'] - if is_input: + if (p_name, var.name) in model._input_vars: node_attrs = INPUT_NODE_ATTRS.copy() edge_attrs = INPUT_EDGE_ATTRS.copy() - elif isinstance(var, DiagnosticVariable): + elif var_type == VarType.ON_DEMAND: node_attrs['style'] = 'diagonals' - elif isinstance(var, ForeignVariable): + elif var_type == VarType.FOREIGN: node_attrs['style'] = 'dashed' edge_attrs['style'] = 'dashed' - elif isinstance(var, (tuple, VariableGroup)): + elif var_type == VarType.GROUP: node_attrs['shape'] = 'box3d' - if not isinstance(var, tuple) and var.provided: + if var_intent == VarIntent.OUT: edge_attrs.update({'arrowhead': 'empty'}) - edge_ends = link_2_node, var_key + edge_ends = p_name, var_key else: - edge_ends = var_key, link_2_node + edge_ends = var_key, p_name - g.node(var_key, label=label, **node_attrs) + g.node(var_key, label=var.name, **node_attrs) g.edge(*edge_ends, weight='200', **edge_attrs) def _add_inputs(g, model): - for proc_name, variables in model._input_vars.items(): - for var_name, var in variables.items(): - _add_var(g, var, var_name, proc_name, is_input=True) + for p_name, var_name in model._input_vars: + p_cls = type(model[p_name]) + var = attr_fields_dict(p_cls)[var_name] + + _add_var(g, model, var, p_name) def _add_variables(g, model): - for proc_name, variables in model._processes.items(): - for var_name, var in variables.items(): - if model.is_input(var): - continue - _add_var(g, var, var_name, proc_name) + for p_name, p_obj in model._processes.items(): + p_cls = type(p_obj) + + for var_name, var in attr_fields_dict(p_cls).items(): + _add_var(g, model, var, p_name) + - if isinstance(var, (tuple, VariableGroup)): - for v in var: - _add_var(g, v, '\', hash_variable(var)) +def _get_target_keys(p_obj, var_name): + return ( + maybe_to_list(p_obj.__xsimlab_store_keys__.get(var_name, [])) + + maybe_to_list(p_obj.__xsimlab_od_keys__.get(var_name, [])) + ) -def _add_var_and_foreign_vars(g, model, proc_name, var_name): - variable = model[proc_name][var_name] +def _add_var_and_targets(g, model, p_name, var_name): + this_p_name = p_name + this_var_name = var_name - if isinstance(variable, ForeignVariable): - variable = variable.ref_var + this_p_obj = model._processes[this_p_name] + this_target_keys = _get_target_keys(this_p_obj, this_var_name) - for p_name, variables in model._processes.items(): - for v_name, var in variables.items(): - if model.is_input(var): - is_input = True - else: - is_input = False + for p_name, p_obj in model._processes.items(): + p_cls = type(p_obj) - if var is variable or getattr(var, 'ref_var', None) is variable: - _add_var(g, var, v_name, p_name, is_input=is_input) - elif isinstance(var, (tuple, VariableGroup)): - for v in var: - if v is variable or getattr(v, 'ref_var', None) is variable: - _add_var(g, var, v_name, p_name, is_input=is_input) - _add_var(g, v, '\', hash_variable(var)) + for var_name, var in attr_fields_dict(p_cls).items(): + target_keys = _get_target_keys(p_obj, var_name) + + if ((p_name, var_name) == (this_p_name, this_var_name) or + len(set(target_keys) & set(this_target_keys))): + _add_var(g, model, var, p_name) def to_graphviz(model, rankdir='LR', show_only_variable=None, @@ -134,18 +131,14 @@ def to_graphviz(model, rankdir='LR', show_only_variable=None, _add_processes(g, model) if show_only_variable is not None: - if isinstance(show_only_variable, AbstractVariable): - proc_name, var_name = model._get_proc_var_name(show_only_variable) - else: - proc_name, var_name = show_only_variable - _add_var_and_foreign_vars(g, model, proc_name, var_name) + p_name, var_name = show_only_variable + _add_var_and_targets(g, model, p_name, var_name) else: - if show_inputs: - _add_inputs(g, model) - if show_variables: _add_variables(g, model) + elif show_inputs: + _add_inputs(g, model) return g @@ -188,27 +181,25 @@ def dot_graph(model, filename=None, format=None, show_only_variable=None, show_inputs=False, show_variables=False, **kwargs): """ Render a model as a graph using dot. - If `filename` is not None, write a file to disk with that name in the - format specified by `format`. `filename` should not include an extension. Parameters ---------- model : object The Model instance to display. filename : str or None, optional - The name (without an extension) of the file to write to disk. If + The name (without an extension) of the file to write to disk. If `filename` is None (default), no file will be written, and we communicate with dot using only pipes. format : {'png', 'pdf', 'dot', 'svg', 'jpeg', 'jpg'}, optional Format in which to write output file. Default is 'png'. - show_only_variable : object or tuple, optional - Show only a variable (and all other linked variables) given either - as a Variable object or a tuple corresponding to process name and - variable name. Deactivated by default. + show_only_variable : tuple, optional + Show only a variable (and all other variables sharing the + same value) given as a tuple ``(process_name, variable_name)``. + Deactivated by default. show_inputs : bool, optional If True, show all input variables in the graph (default: False). Ignored if `show_only_variable` is not None. - show_variabless : bool, optional + show_variables : bool, optional If True, show also the other variables (default: False). Ignored if `show_only_variable` is not None. **kwargs @@ -216,7 +207,8 @@ def dot_graph(model, filename=None, format=None, show_only_variable=None, Returns ------- - result : None or IPython.display.Image or IPython.display.SVG (See below.) + result : None or IPython.display.Image or IPython.display.SVG + (See below.) Notes ----- diff --git a/xsimlab/model.py b/xsimlab/model.py index 51c8c6c9..b89dfbc9 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -412,10 +412,10 @@ def visualize(self, show_only_variable=None, show_inputs=False, Parameters ---------- - show_only_variable : object or tuple, optional - Show only a variable (and all other linked variables) given either - as a Variable object or a tuple corresponding to process name and - variable name. Deactivated by default. + show_only_variable : tuple, optional + Show only a variable (and all other variables sharing the + same value) given as a tuple ``(process_name, variable_name)``. + Deactivated by default. show_inputs : bool, optional If True, show all input variables in the graph (default: False). Ignored if `show_only_variable` is not None. diff --git a/xsimlab/utils.py b/xsimlab/utils.py index b2acb30b..47f635f0 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -29,6 +29,10 @@ def has_method(obj, meth): return callable(getattr(obj, meth, False)) +def maybe_to_list(obj): + return obj if isinstance(obj, list) else [obj] + + def import_required(mod_name, error_msg): """Attempt to import a required dependency. Raises a RuntimeError if the requested module is not available. From 96c067c4a43a9c81f524182c53816d6b6edf75b5 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 00:33:53 +0200 Subject: [PATCH 45/97] rename snapshot_vars to output_vars --- doc/api.rst | 2 +- xsimlab/xr_accessor.py | 77 ++++++++++++++++++++--------------------- xsimlab/xr_interface.py | 14 ++++---- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 1d85f61c..4a975e8c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -40,7 +40,7 @@ properties listed below. Proper use of this accessor should be like: Dataset.xsimlab.clock_coords Dataset.xsimlab.master_clock_dim - Dataset.xsimlab.snapshot_vars + Dataset.xsimlab.output_vars **Methods** diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 6fd83907..ddaa5066 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -7,7 +7,6 @@ import numpy as np from xarray import Dataset, register_dataset_accessor -from .process import Process from .model import Model from .xr_interface import DatasetModelInterface @@ -48,7 +47,7 @@ class SimlabAccessor(object): _clock_key = '_xsimlab_snapshot_clock' _master_clock_key = '_xsimlab_master_clock' - _snapshot_vars_key = '_xsimlab_snapshot_vars' + _output_vars_key = '_xsimlab_output_vars' def __init__(self, xarray_obj): self._obj = xarray_obj @@ -57,7 +56,7 @@ def __init__(self, xarray_obj): @property def clock_coords(self): """Dictionary of :class:`xarray.DataArray` objects corresponding to - clock coordinates (including master clock and snapshot clocks). + clock coordinates. """ return {k: coord for k, coord in self._obj.coords.items() if self._clock_key in coord.attrs} @@ -214,7 +213,7 @@ def _set_input_vars(self, model, process, **inputs): xr_var.dims = dims self._obj[xr_var_name] = xr_var - def _set_snapshot_vars(self, model, clock_dim, **process_vars): + def _set_output_vars(self, model, clock_dim, **process_vars): xr_vars_list = [] for proc_name, vars in sorted(process_vars.items()): @@ -230,19 +229,19 @@ def _set_snapshot_vars(self, model, clock_dim, **process_vars): % (proc_name, var_name)) xr_vars_list.append(proc_name + '__' + var_name) - snapshot_vars = ','.join(xr_vars_list) + output_vars = ','.join(xr_vars_list) if clock_dim is None: - self._obj.attrs[self._snapshot_vars_key] = snapshot_vars + self._obj.attrs[self._output_vars_key] = output_vars else: if clock_dim not in self.clock_coords: raise ValueError("%r coordinate is not a valid clock " "coordinate. " % clock_dim) coord = self.clock_coords[clock_dim] - coord.attrs[self._snapshot_vars_key] = snapshot_vars + coord.attrs[self._output_vars_key] = output_vars - def _get_snapshot_vars(self, name, obj): - vars_str = obj.attrs.get(self._snapshot_vars_key, '') + def _get_output_vars(self, name, obj): + vars_str = obj.attrs.get(self._output_vars_key, '') if vars_str: return {name: [tuple(s.split('__')) for s in vars_str.split(',')]} @@ -250,16 +249,16 @@ def _get_snapshot_vars(self, name, obj): return {} @property - def snapshot_vars(self): + def output_vars(self): """Returns a dictionary of snapshot clock dimension names as keys and - snapshot variable names - i.e. lists of (process name, variable name) + output variable names - i.e. lists of (process name, variable name) tuples - as values. """ - snapshot_vars = {} + output_vars = {} for cname, coord in self._obj.coords.items(): - snapshot_vars.update(self._get_snapshot_vars(cname, coord)) - snapshot_vars.update(self._get_snapshot_vars(None, self._obj)) - return snapshot_vars + output_vars.update(self._get_output_vars(cname, coord)) + output_vars.update(self._get_output_vars(None, self._obj)) + return output_vars def update_clocks(self, model=None, clocks=None, master_clock=None): """Update clock coordinates. @@ -323,20 +322,20 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): for dim, kwargs in clocks.items(): ds.xsimlab._set_snapshot_clock(dim, **kwargs) - for dim, var_list in self.snapshot_vars.items(): + for dim, var_list in self.output_vars.items(): var_dict = defaultdict(list) for proc_name, var_name in var_list: var_dict[proc_name].append(var_name) if dim is None or dim in ds: - ds.xsimlab._set_snapshot_vars(model, dim, **var_dict) + ds.xsimlab._set_output_vars(model, dim, **var_dict) return ds - def update_vars(self, model=None, input_vars=None, snapshot_vars=None): - """Update model input values and/or snapshot variable names. + def update_vars(self, model=None, input_vars=None, output_vars=None): + """Update model input values and/or output variable names. - Add or replace all input values (resp. snapshot variable names) per + Add or replace all input values (resp. output variable names) per given process (resp. clock coordinate). More details about the values allowed for the parameters below can be @@ -348,8 +347,8 @@ def update_vars(self, model=None, input_vars=None, snapshot_vars=None): Reference model. If None, tries to get model from context. input_vars : dict of dicts, optional Model input values given per process. - snapshot_vars : dict of dicts, optional - Model variables to save as simulation snapshots, given per + output_vars : dict of dicts, optional + Model variables to save as simulation output, given per clock coordinate. Returns @@ -371,9 +370,9 @@ def update_vars(self, model=None, input_vars=None, snapshot_vars=None): for proc_name, vars in input_vars.items(): ds.xsimlab._set_input_vars(model, proc_name, **vars) - if snapshot_vars is not None: - for dim, proc_vars in snapshot_vars.items(): - ds.xsimlab._set_snapshot_vars(model, dim, **proc_vars) + if output_vars is not None: + for dim, proc_vars in output_vars.items(): + ds.xsimlab._set_output_vars(model, dim, **proc_vars) return ds @@ -418,13 +417,13 @@ def filter_vars(self, model=None): ds = self._obj.drop(drop_variables) - for dim, var_list in self.snapshot_vars.items(): + for dim, var_list in self.output_vars.items(): var_dict = defaultdict(list) for proc_name, var_name in var_list: if model.get(proc_name, {}).get(var_name, False): var_dict[proc_name].append(var_name) - ds.xsimlab._set_snapshot_vars(model, dim, **var_dict) + ds.xsimlab._set_output_vars(model, dim, **var_dict) return ds @@ -443,7 +442,7 @@ def run(self, model=None, safe_mode=True): Returns ------- output : Dataset - Another Dataset with both model inputs and outputs (snapshots). + Another Dataset with both model inputs and outputs. """ model = _maybe_get_model_from_context(model) @@ -470,7 +469,7 @@ def run_multi(self): def create_setup(model=None, clocks=None, master_clock=None, - input_vars=None, snapshot_vars=None): + input_vars=None, output_vars=None): """Create a specific setup for model runs. This convenient function creates a new :class:`xarray.Dataset` object with @@ -499,16 +498,16 @@ def create_setup(model=None, clocks=None, master_clock=None, Values are anything that can be easily converted to :class:`xarray.Variable` objects, e.g., single values, array-like, (dims, data, attrs) tuples or xarray objects. - snapshot_vars : dict of dicts, optional - Model variables to save as simulation snapshots, given per clock + output_vars : dict of dicts, optional + Model variables to save as simulation output, given per clock coordinate. The structure of the dict of dicts looks like ``{'dim': {'process_name': 'var_name', ...}, ...}``. - 'dim' must correspond to the dimension of a clock coordinate or None - for snapshots that only have to be taken once at the end of the - simulation. - To take snapshots for multiple variables that belong to the same - process, a tuple of multiple variable names can be given instead of a - string. + If ``'dim'`` corresponds to the dimension of a clock coordinate, + snapshot values will be recorded at each time given by the coordinate + labels. if None is given, only one value will be recorded at the + end of the simulation. + Note that instead of ``'var_name'``, a tuple of multiple variable names + (declared in the same process) can be given. Returns ------- @@ -545,7 +544,7 @@ def create_setup(model=None, clocks=None, master_clock=None, in the returned Dataset, using their default value (if any). It requires that their process are provided as keys of ``input_vars``, though. - Snapshot variable names are added in Dataset as specific attributes + Output variable names are added in Dataset as specific attributes (global and/or clock coordinate attributes). """ @@ -555,6 +554,6 @@ def create_setup(model=None, clocks=None, master_clock=None, .xsimlab.update_clocks(model=model, clocks=clocks, master_clock=master_clock) .xsimlab.update_vars(model=model, input_vars=input_vars, - snapshot_vars=snapshot_vars)) + output_vars=output_vars)) return ds diff --git a/xsimlab/xr_interface.py b/xsimlab/xr_interface.py index 95b2e643..c79c0ca7 100644 --- a/xsimlab/xr_interface.py +++ b/xsimlab/xr_interface.py @@ -82,16 +82,16 @@ def init_snapshots(self): """Initialize snapshots for model variables given in attributes of Dataset. """ - self.snapshot_vars = self.dataset.xsimlab.snapshot_vars + self.output_vars = self.dataset.xsimlab.output_vars self.snapshot_values = {} - for vars in self.snapshot_vars.values(): + for vars in self.output_vars.values(): self.snapshot_values.update({v: [] for v in vars}) self.snapshot_save = { clock: np.in1d(self.dataset[self.master_clock_dim].values, self.dataset[clock].values) - for clock in self.snapshot_vars if clock is not None + for clock in self.output_vars if clock is not None } def take_snapshot_var(self, key): @@ -104,7 +104,7 @@ def take_snapshot_var(self, key): def take_snapshots(self, istep): """Take snapshots at a given step index.""" - for clock, vars in self.snapshot_vars.items(): + for clock, vars in self.output_vars.items(): if clock is None: if istep == -1: for key in vars: @@ -145,7 +145,7 @@ def get_output_dataset(self): xr_variables = {} - for clock, vars in self.snapshot_vars.items(): + for clock, vars in self.output_vars.items(): for key in vars: var_name = '__'.join(key) xr_variables[var_name] = self.snapshot_to_xarray_variable( @@ -154,12 +154,12 @@ def get_output_dataset(self): out_ds = self.dataset.update(xr_variables, inplace=False) - for clock in self.snapshot_vars: + for clock in self.output_vars: if clock is None: attrs = out_ds.attrs else: attrs = out_ds[clock].attrs - attrs.pop(SimlabAccessor._snapshot_vars_key) + attrs.pop(SimlabAccessor._output_vars_key) return out_ds From 539bc99405bcedde0c5b3f03f46bae11699bbc69 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 00:58:28 +0200 Subject: [PATCH 46/97] update what's new --- doc/whats_new.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 27178dcc..d4fb8f8b 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -74,6 +74,10 @@ changes are effective now! ``show_variables=True`` now shows all model variables including inputs. +- For simplicity, ``Dataset.xsimlab.snapshot_vars`` has been renamed to + ``output_vars``. The corresponding arguments in ``create_setup`` and + ``Dataset.xsimlab.update_vars`` have been renamed accordingly. + Enhancements ~~~~~~~~~~~~ @@ -89,8 +93,18 @@ Enhancements intended. - Some more sanity checks have been added when creating process classes. +- Simulation active and output data r/w access has been refactored + internally so that it should be easy to later support alternative + data storage backends (e.g., on-disk, distributed). - Added ``Model.dependent_processes`` property (so far this was not - public API). + in public API). + +Regressions (will be fixed in future releases) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Although it is possible to set validators, converters and/or default + values for variables (this is directly supported by ``attrs``), these + are not handled by xarray-simlab yet. v0.1.1 (20 November 2017) ------------------------- From ed910050f73ed4e82fde43d77a844a8d5c7f10d6 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 01:01:27 +0200 Subject: [PATCH 47/97] rename self._obj to self._ds --- xsimlab/xr_accessor.py | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index ddaa5066..f6b8983a 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -49,8 +49,8 @@ class SimlabAccessor(object): _master_clock_key = '_xsimlab_master_clock' _output_vars_key = '_xsimlab_output_vars' - def __init__(self, xarray_obj): - self._obj = xarray_obj + def __init__(self, ds): + self._ds = ds self._master_clock_dim = None @property @@ -58,7 +58,7 @@ def clock_coords(self): """Dictionary of :class:`xarray.DataArray` objects corresponding to clock coordinates. """ - return {k: coord for k, coord in self._obj.coords.items() + return {k: coord for k, coord in self._ds.coords.items() if self._clock_key in coord.attrs} @property @@ -74,7 +74,7 @@ def master_clock_dim(self): if self._master_clock_dim is not None: return self._master_clock_dim else: - for c in self._obj.coords.values(): + for c in self._ds.coords.values(): if c.attrs.get(self._master_clock_key, False): dim = c.dims[0] self._master_clock_dim = dim @@ -82,17 +82,17 @@ def master_clock_dim(self): return None def _set_master_clock_dim(self, dim): - if dim not in self._obj.coords: + if dim not in self._ds.coords: raise KeyError("Dataset has no %r dimension coordinate. " "To create a new master clock dimension, " "use Dataset.xsimlab.update_clock." % dim) if self.master_clock_dim is not None: - self._obj[self.master_clock_dim].attrs.pop(self._master_clock_key) + self._ds[self.master_clock_dim].attrs.pop(self._master_clock_key) - self._obj[dim].attrs[self._clock_key] = np.uint8(True) - self._obj[dim].attrs[self._master_clock_key] = np.uint8(True) + self._ds[dim].attrs[self._clock_key] = np.uint8(True) + self._ds[dim].attrs[self._master_clock_key] = np.uint8(True) self._master_clock_dim = dim def _set_clock_data(self, dim, data, start, end, step, nsteps): @@ -123,15 +123,15 @@ def _set_clock_data(self, dim, data, start, end, step, nsteps): def _set_master_clock(self, dim, data=None, start=0., end=None, step=None, nsteps=None, units=None, calendar=None): - if dim in self._obj.dims: + if dim in self._ds.dims: raise ValueError("dimension %r already exists" % dim) - self._obj[dim] = self._set_clock_data(dim, data, start, end, + self._ds[dim] = self._set_clock_data(dim, data, start, end, step, nsteps) if units is not None: - self._obj[dim].attrs['units'] = units + self._ds[dim].attrs['units'] = units if calendar is not None: - self._obj[dim].attrs['calendar'] = calendar + self._ds[dim].attrs['calendar'] = calendar self._set_master_clock_dim(dim) @@ -144,7 +144,7 @@ def _set_snapshot_clock(self, dim, data=None, start=0., end=None, clock_data = self._set_clock_data(dim, data, start, end, step, nsteps) - da_master_clock = self._obj[self.master_clock_dim] + da_master_clock = self._ds[self.master_clock_dim] if auto_adjust: kwargs = {'method': 'nearest'} @@ -155,14 +155,14 @@ def _set_snapshot_clock(self, dim, data=None, start=0., end=None, kwargs.update(indexer) da_snapshot_clock = da_master_clock.sel(**kwargs) - self._obj[dim] = da_snapshot_clock.rename({self.master_clock_dim: dim}) + self._ds[dim] = da_snapshot_clock.rename({self.master_clock_dim: dim}) # .sel copies variable attributes - self._obj[dim].attrs.pop(self._master_clock_key) + self._ds[dim].attrs.pop(self._master_clock_key) for attr_name in ('units', 'calendar'): attr_value = da_master_clock.attrs.get(attr_name) if attr_value is not None: - self._obj[dim].attrs[attr_name] = attr_value + self._ds[dim].attrs[attr_name] = attr_value def _set_input_vars(self, model, process, **inputs): if isinstance(process, Process): @@ -211,7 +211,7 @@ def _set_input_vars(self, model, process, **inputs): rename_dict = {'this_variable': xr_var_name} dims = tuple(rename_dict.get(dim, dim) for dim in xr_var.dims) xr_var.dims = dims - self._obj[xr_var_name] = xr_var + self._ds[xr_var_name] = xr_var def _set_output_vars(self, model, clock_dim, **process_vars): xr_vars_list = [] @@ -232,7 +232,7 @@ def _set_output_vars(self, model, clock_dim, **process_vars): output_vars = ','.join(xr_vars_list) if clock_dim is None: - self._obj.attrs[self._output_vars_key] = output_vars + self._ds.attrs[self._output_vars_key] = output_vars else: if clock_dim not in self.clock_coords: raise ValueError("%r coordinate is not a valid clock " @@ -255,9 +255,9 @@ def output_vars(self): tuples - as values. """ output_vars = {} - for cname, coord in self._obj.coords.items(): + for cname, coord in self._ds.coords.items(): output_vars.update(self._get_output_vars(cname, coord)) - output_vars.update(self._get_output_vars(None, self._obj)) + output_vars.update(self._get_output_vars(None, self._ds)) return output_vars def update_clocks(self, model=None, clocks=None, master_clock=None): @@ -292,7 +292,7 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): """ model = _maybe_get_model_from_context(model) - ds = self._obj.drop(self.clock_coords) + ds = self._ds.drop(self.clock_coords) attrs_master_clock = {} @@ -364,7 +364,7 @@ def update_vars(self, model=None, input_vars=None, output_vars=None): """ model = _maybe_get_model_from_context(model) - ds = self._obj.copy() + ds = self._ds.copy() if input_vars is not None: for proc_name, vars in input_vars.items(): @@ -404,7 +404,7 @@ def filter_vars(self, model=None): drop_variables = [] - for xr_var_name in self._obj: + for xr_var_name in self._ds: if xr_var_name in self.clock_coords: continue try: @@ -415,7 +415,7 @@ def filter_vars(self, model=None): if not model.is_input((proc_name, var_name)): drop_variables.append(xr_var_name) - ds = self._obj.drop(drop_variables) + ds = self._ds.drop(drop_variables) for dim, var_list in self.output_vars.items(): var_dict = defaultdict(list) @@ -450,7 +450,7 @@ def run(self, model=None, safe_mode=True): if safe_mode: model = model.clone() - ds_model_interface = DatasetModelInterface(model, self._obj) + ds_model_interface = DatasetModelInterface(model, self._ds) out_ds = ds_model_interface.run_model() return out_ds From 5fa752f89d9bab3b2976742d896f4c51e3881504 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 02:23:10 +0200 Subject: [PATCH 48/97] create a graph builder class --- xsimlab/dot.py | 145 ++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/xsimlab/dot.py b/xsimlab/dot.py index 1dd71670..1d6241e4 100644 --- a/xsimlab/dot.py +++ b/xsimlab/dot.py @@ -32,92 +32,100 @@ VAR_EDGE_ATTRS = {'arrowhead': 'none', 'color': '#555555'} -def hash_variable(var): +def _hash_variable(var): # issue with variables with the same name declared in different processes # return str(hash(var)) return str(id(var)) -def _add_processes(g, model): - seen = set() +def _get_target_keys(p_obj, var_name): + return ( + maybe_to_list(p_obj.__xsimlab_store_keys__.get(var_name, [])) + + maybe_to_list(p_obj.__xsimlab_od_keys__.get(var_name, [])) + ) - for p_name, p_obj in model._processes.items(): - if p_name not in seen: - seen.add(p_name) - g.node(p_name, label=p_name, **PROC_NODE_ATTRS) - for dep_p_name in model.dependent_processes[p_name]: - g.edge(dep_p_name, p_name, **PROC_EDGE_ATTRS) +class _GraphBuilder(object): + def __init__(self, model, graph_attr): + self.model = model + self.g = graphviz.Digraph(graph_attr=graph_attr) -def _add_var(g, model, var, p_name): - node_attrs = VAR_NODE_ATTRS.copy() - edge_attrs = VAR_EDGE_ATTRS.copy() + def add_processes(self): + seen = set() - var_key = hash_variable(var) - var_intent = var.metadata['intent'] - var_type = var.metadata['var_type'] + for p_name, p_obj in self.model._processes.items(): + if p_name not in seen: + seen.add(p_name) + self.g.node(p_name, label=p_name, **PROC_NODE_ATTRS) - if (p_name, var.name) in model._input_vars: - node_attrs = INPUT_NODE_ATTRS.copy() - edge_attrs = INPUT_EDGE_ATTRS.copy() - elif var_type == VarType.ON_DEMAND: - node_attrs['style'] = 'diagonals' - elif var_type == VarType.FOREIGN: - node_attrs['style'] = 'dashed' - edge_attrs['style'] = 'dashed' - elif var_type == VarType.GROUP: - node_attrs['shape'] = 'box3d' + for dep_p_name in self.model.dependent_processes[p_name]: + self.g.edge(dep_p_name, p_name, **PROC_EDGE_ATTRS) - if var_intent == VarIntent.OUT: - edge_attrs.update({'arrowhead': 'empty'}) - edge_ends = p_name, var_key - else: - edge_ends = var_key, p_name + def _add_var(self, var, p_name): + if (p_name, var.name) in self.model._input_vars: + node_attrs = INPUT_NODE_ATTRS.copy() + edge_attrs = INPUT_EDGE_ATTRS.copy() + else: + node_attrs = VAR_NODE_ATTRS.copy() + edge_attrs = VAR_EDGE_ATTRS.copy() - g.node(var_key, label=var.name, **node_attrs) - g.edge(*edge_ends, weight='200', **edge_attrs) + var_key = _hash_variable(var) + var_intent = var.metadata['intent'] + var_type = var.metadata['var_type'] + if var_type == VarType.ON_DEMAND: + node_attrs['style'] = 'diagonals' -def _add_inputs(g, model): - for p_name, var_name in model._input_vars: - p_cls = type(model[p_name]) - var = attr_fields_dict(p_cls)[var_name] + elif var_type == VarType.FOREIGN: + node_attrs['style'] = 'dashed' + edge_attrs['style'] = 'dashed' - _add_var(g, model, var, p_name) + elif var_type == VarType.GROUP: + node_attrs['shape'] = 'box3d' + if var_intent == VarIntent.OUT: + edge_attrs.update({'arrowhead': 'empty'}) + edge_ends = p_name, var_key + else: + edge_ends = var_key, p_name -def _add_variables(g, model): - for p_name, p_obj in model._processes.items(): - p_cls = type(p_obj) + self.g.node(var_key, label=var.name, **node_attrs) + self.g.edge(*edge_ends, weight='200', **edge_attrs) - for var_name, var in attr_fields_dict(p_cls).items(): - _add_var(g, model, var, p_name) + def add_inputs(self): + for p_name, var_name in self.model._input_vars: + p_cls = type(self.model[p_name]) + var = attr_fields_dict(p_cls)[var_name] + self._add_var(var, p_name) -def _get_target_keys(p_obj, var_name): - return ( - maybe_to_list(p_obj.__xsimlab_store_keys__.get(var_name, [])) + - maybe_to_list(p_obj.__xsimlab_od_keys__.get(var_name, [])) - ) + def add_variables(self): + for p_name, p_obj in self.model._processes.items(): + p_cls = type(p_obj) + + for var_name, var in attr_fields_dict(p_cls).items(): + self._add_var(var, p_name) + def add_var_and_targets(self, p_name, var_name): + this_p_name = p_name + this_var_name = var_name -def _add_var_and_targets(g, model, p_name, var_name): - this_p_name = p_name - this_var_name = var_name + this_p_obj = self.model._processes[this_p_name] + this_target_keys = _get_target_keys(this_p_obj, this_var_name) - this_p_obj = model._processes[this_p_name] - this_target_keys = _get_target_keys(this_p_obj, this_var_name) + for p_name, p_obj in self.model._processes.items(): + p_cls = type(p_obj) - for p_name, p_obj in model._processes.items(): - p_cls = type(p_obj) + for var_name, var in attr_fields_dict(p_cls).items(): + target_keys = _get_target_keys(p_obj, var_name) - for var_name, var in attr_fields_dict(p_cls).items(): - target_keys = _get_target_keys(p_obj, var_name) + if ((p_name, var_name) == (this_p_name, this_var_name) or + len(set(target_keys) & set(this_target_keys))): + self._add_var(var, p_name) - if ((p_name, var_name) == (this_p_name, this_var_name) or - len(set(target_keys) & set(this_target_keys))): - _add_var(g, model, var, p_name) + def get_graph(self): + return self.g def to_graphviz(model, rankdir='LR', show_only_variable=None, @@ -126,21 +134,22 @@ def to_graphviz(model, rankdir='LR', show_only_variable=None, graph_attr = graph_attr or {} graph_attr['rankdir'] = rankdir graph_attr.update(kwargs) - g = graphviz.Digraph(graph_attr=graph_attr) - _add_processes(g, model) + builder = _GraphBuilder(model, graph_attr) + + builder.add_processes() if show_only_variable is not None: p_name, var_name = show_only_variable - _add_var_and_targets(g, model, p_name, var_name) + builder.add_var_and_targets(p_name, var_name) - else: - if show_variables: - _add_variables(g, model) - elif show_inputs: - _add_inputs(g, model) + elif show_variables: + builder.add_variables() + + elif show_inputs: + builder.add_inputs() - return g + return builder.get_graph() IPYTHON_IMAGE_FORMATS = frozenset(['jpeg', 'png']) From ca24948b339fc4d5ddad8dbf2323e6912bd642d1 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 02:34:11 +0200 Subject: [PATCH 49/97] remove combomethod decorator (not used anymore) --- xsimlab/utils.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/xsimlab/utils.py b/xsimlab/utils.py index 47f635f0..c2e2324b 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -43,20 +43,6 @@ def import_required(mod_name, error_msg): raise RuntimeError(error_msg) -class combomethod(object): - def __init__(self, method): - self.method = method - - def __get__(self, obj=None, objtype=None): - @wraps(self.method) - def _wrapper(*args, **kwargs): - if obj is not None: - return self.method(obj, *args, **kwargs) - else: - return self.method(objtype, *args, **kwargs) - return _wrapper - - class AttrMapping(object): """A class similar to `collections.abc.Mapping`, which also allows getting keys with attribute access. @@ -76,6 +62,7 @@ class AttrMapping(object): https://www.python.org/ """ + # TODO: use abc.ABCMeta now that metaclasses are not used anymore? _initialized = False def __init__(self, mapping=None): From 5c0821176967d77462da7f9e7159b2d812c89bdf Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 15:04:32 +0200 Subject: [PATCH 50/97] (wip) refactor dataset/model interface as simulation drivers --- xsimlab/xr_accessor.py | 14 ++- xsimlab/xr_interface.py | 254 ++++++++++++++++++++++++---------------- 2 files changed, 163 insertions(+), 105 deletions(-) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index f6b8983a..972b0c33 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -8,7 +8,7 @@ from xarray import Dataset, register_dataset_accessor from .model import Model -from .xr_interface import DatasetModelInterface +from .xr_interface import XarraySimulationDriver @register_dataset_accessor('filter') @@ -127,7 +127,7 @@ def _set_master_clock(self, dim, data=None, start=0., end=None, raise ValueError("dimension %r already exists" % dim) self._ds[dim] = self._set_clock_data(dim, data, start, end, - step, nsteps) + step, nsteps) if units is not None: self._ds[dim].attrs['units'] = units if calendar is not None: @@ -450,8 +450,14 @@ def run(self, model=None, safe_mode=True): if safe_mode: model = model.clone() - ds_model_interface = DatasetModelInterface(model, self._ds) - out_ds = ds_model_interface.run_model() + store = {} + output_store = defaultdict(list) + + sim_driver = XarraySimulationDriver(model, self._ds, + store, output_store) + + out_ds = sim_driver.run_model() + return out_ds def run_multi(self): diff --git a/xsimlab/xr_interface.py b/xsimlab/xr_interface.py index c79c0ca7..a6d0d672 100644 --- a/xsimlab/xr_interface.py +++ b/xsimlab/xr_interface.py @@ -1,6 +1,10 @@ +import copy + import numpy as np import xarray as xr +from .utils import attr_fields_dict + def _get_dims_from_variable(array, variable): """Given an array of values (snapshot) and a (xarray-simlab) Variable @@ -11,116 +15,160 @@ def _get_dims_from_variable(array, variable): return tuple() -class DatasetModelInterface(object): - """Interface between xarray.Dataset and Model. +class BaseSimulationDriver(object): + """Base class that provides a minimal interface for creating + simulation drivers (should be inherited). + + It also implements methods for binding a simulation data store to + a model and for updating both this active data store and the + simulation output store. + + """ + + def __init__(self, model, store, output_store): + self.model = model + self.store = store + self.output_store = output_store + + self._bind_store_to_model() + + def _bind_store_to_model(self): + """Bind the simulation active data store to each process in the + model. + """ + for p_obj in self.model.values(): + p_obj.__xsimlab_store__ = self.store + + def update_store(self, input_vars): + """Update the simulation active data store with input variable + values. - It is used to: + ``input_vars`` is a dictionary where keys are store keys, i.e., + ``(process_name, var_name)`` tuples, and values are the input + values to set in the store. - - set model inputs using the variables of a Dataset object, - - run model simulation stages, - - take snapshots for given model variables (defined in attributes of - Dataset) following one or several clocks (i.e., Dataset coordinates), - - convert the snapshots back into xarray.Variable objects and return a - new xarray.Dataset object. + Values are first copied from ``input_vars`` before being put in + the store to prevent weird behavior (as model processes might + update in-place the values in the store). + + Entries of ``input_vars`` that doesn't correspond to model + inputs are silently ignored. + + """ + for key in self.model.input_vars: + value = input_vars.get(key) + + if value is not None: + self.store[key] = copy(value) + + def update_output_store(self, output_var_keys): + """Update the simulation output store (i.e., append new values to the + store) from snapshots of variables given in ``output_var_keys`` list. + """ + for key in output_var_keys: + p_name, var_name = key + p_obj = self.model._processes[p_name] + value = getattr(p_obj, var_name) + + self.output_store.append(key, value) + + def run_model(self): + """Main function of the driver used to run a simulation (must be + implemented in subclasses). + """ + raise NotImplementedError() + + +class XarraySimulationDriver(BaseSimulationDriver): + """Simulation driver using xarray.Dataset objects as I/O. + + - Performs some sanity checks on the content of the given input Dataset. + - Sets model inputs from the input Dataset. + - Saves model outputs for given model variables (defined in attributes of + Dataset) following one or several clocks (i.e., Dataset coordinates). + - Gets simulation results as a new xarray.Dataset object. """ - def __init__(self, model, dataset): + def __init__(self, dataset, model, store, output_store): self.model = model - self.dataset = dataset + + super(XarraySimulationDriver, self).__init__(model, store, + output_store) + + self.output_vars = dataset.xsimlab.output_vars + self.output_save_steps = self._get_output_save_steps() self.master_clock_dim = dataset.xsimlab.master_clock_dim if self.master_clock_dim is None: - raise ValueError("missing master clock dimension / coordinate ") + raise ValueError("Missing master clock dimension / coordinate") - self.check_model_inputs_in_dataset() + self._check_missing_model_inputs() - def check_model_inputs_in_dataset(self): + def _check_missing_model_inputs(self): """Check if all model inputs have their corresponding data variables - in Dataset. + in the input Dataset. """ missing_data_vars = [] - for proc_name, vars in self.model.input_vars.items(): - for var_name, var in vars.items(): - xr_var_name = proc_name + '__' + var_name - if xr_var_name not in self.dataset.data_vars: - missing_data_vars.append(xr_var_name) + for p_name, var_name in self.model.input_vars: + xr_var_name = p_name + '__' + var_name + + if xr_var_name not in self.dataset.data_vars: + missing_data_vars.append(xr_var_name) if missing_data_vars: - raise KeyError("missing data variables %s in Dataset" + raise KeyError("Missing data variables %s in Dataset" % missing_data_vars) - def set_model_inputs(self, dataset): - """Set model inputs values from a given Dataset object (may be a subset - of self.dataset).""" - for proc_name, vars in self.model.input_vars.items(): - for var_name, var in vars.items(): - xr_var_name = proc_name + '__' + var_name - xr_var = dataset.get(xr_var_name) - if xr_var is not None: - var.value = xr_var.values.copy() - - def split_data_vars_clock(self): - """Separate in Dataset between data variables that have the master clock - dimension and those that don't. - """ - ds_clock = self.dataset.filter( - lambda v: self.master_clock_dim in v.dims - ) - ds_no_clock = self.dataset.filter( - lambda v: self.master_clock_dim not in v.dims - ) - return ds_clock, ds_no_clock - - @property - def time_step_lengths(self): - """Return a DataArray with time-step durations.""" - clock_coord = self.dataset[self.master_clock_dim] - return clock_coord.diff(self.master_clock_dim).values - - def init_snapshots(self): - """Initialize snapshots for model variables given in attributes of - Dataset. + def _get_output_save_steps(self): + """Returns a dictionary where keys are names of clock coordinates and + values are numpy boolean arrays that specify whether or not to + save outputs at every step of a simulation. """ - self.output_vars = self.dataset.xsimlab.output_vars + save_steps = {} - self.snapshot_values = {} - for vars in self.output_vars.values(): - self.snapshot_values.update({v: [] for v in vars}) + for clock in self.output_vars: + if clock is None: + continue - self.snapshot_save = { - clock: np.in1d(self.dataset[self.master_clock_dim].values, - self.dataset[clock].values) - for clock in self.output_vars if clock is not None - } + elif clock == self.master_clock_dim: + save_steps[clock] = np.ones_like( + self.dataset[self.master_clock_dim].values, dtype=bool) - def take_snapshot_var(self, key): - """Take a snapshot of a given model variable (i.e., a copy of the value - of its `state` property). - """ - proc_name, var_name = key - model_var = self.model._processes[proc_name]._variables[var_name] - self.snapshot_values[key].append(np.array(model_var.state)) + else: + save_steps[clock] = np.in1d( + self.dataset[self.master_clock_dim].values, + self.dataset[clock].values) - def take_snapshots(self, istep): - """Take snapshots at a given step index.""" - for clock, vars in self.output_vars.items(): - if clock is None: - if istep == -1: - for key in vars: - self.take_snapshot_var(key) - elif self.snapshot_save[clock][istep]: - for key in vars: - self.take_snapshot_var(key) + return save_steps + + def _set_input_vars(self, dataset): + for p_name, var_name in self.model.input_vars: + xr_var_name = p_name + '__' + var_name + xr_var = dataset.get(xr_var_name) + + if xr_var is not None: + self.store[(p_name, var_name)] = xr_var.data.copy() + + def _maybe_save_output_vars(self, istep): + if istep == -1: + var_keys = self.output_vars.get(None, []) + self.update_output_store(var_keys) + + else: + for clock, var_keys in self.output_vars.items(): + if clock is None and self.snapshot_save[clock][istep]: + self.update_output_store(var_keys) def snapshot_to_xarray_variable(self, key, clock=None): """Convert snapshots taken for a specific model variable to an xarray.Variable object. """ - proc_name, var_name = key - variable = self.model._processes[proc_name]._variables[var_name] + p_name, var_name = key + p_obj = self.model[p_name] + variable = attr_fields_dict(p_obj)[var_name] - array_list = self.snapshot_values[key] + array_list = self.output_store[key] first_array = array_list[0] if len(array_list) == 1: @@ -137,7 +185,7 @@ def snapshot_to_xarray_variable(self, key, clock=None): return xr.Variable(dims, data, attrs=attrs) - def get_output_dataset(self): + def create_output_dataset(self): """Build a new output Dataset from the input Dataset and all snapshots taken during a model run. """ @@ -164,34 +212,38 @@ def get_output_dataset(self): return out_ds def run_model(self): - """Run the model. + """Run the model and return a new Dataset with all the simulation + inputs and outputs. - The is the main function of the interface. It set model inputs - from the input Dataset, run the simulation stages one after - each other, possibly sets time-dependent values provided for - model inputs (if any) before each time step, take snaphots - between the 'run_step' and the 'finalize_step' stages, and - finally returns a new Dataset with all the inputs and the - snapshots. + - Set model inputs from the input Dataset (update + time-dependent model inputs -- if any -- before each time step). + - Save outputs (snapshots) between the 'run_step' and the + 'finalize_step' stages or at the end of the simulation. """ - ds_clock, ds_no_clock = self.split_data_vars_clock() - ds_clock_any = bool(ds_clock.data_vars) + ds_in = self.dataset.filter( + lambda var: self.master_clock_dim not in var.dims) + ds_in_clock = self.dataset.filter( + lambda var: self.master_clock_dim in var.dims) + + has_clock_inputs = bool(ds_in_clock.data_vars) + + mclock = self.dataset[self.master_clock_dim] + da_dt = mclock.diff(self.master_clock_dim) - self.init_snapshots() - self.set_model_inputs(ds_no_clock) + self._set_input_vars(ds_in) self.model.initialize() - for istep, dt in enumerate(self.time_step_lengths): - if ds_clock_any: - ds_step = ds_clock.isel(**{self.master_clock_dim: istep}) - self.set_model_inputs(ds_step) + for istep, dt in enumerate(da_dt): + if has_clock_inputs: + ds_in_step = ds_in_clock.isel(**{self.master_clock_dim: istep}) + self._set_input_vars(ds_in_step) self.model.run_step(dt) - self.take_snapshots(istep) + self._maybe_save_output_vars(istep) self.model.finalize_step() - self.take_snapshots(-1) + self._maybe_save_output_vars(-1) self.model.finalize() - return self.get_output_dataset() + return self.create_output_dataset() From de2366c6228987ac55bb9c637b355fa8caec1d71 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 15:07:46 +0200 Subject: [PATCH 51/97] rename xr_interface module to drivers (simulation drivers) --- xsimlab/{xr_interface.py => drivers.py} | 0 xsimlab/xr_accessor.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename xsimlab/{xr_interface.py => drivers.py} (100%) diff --git a/xsimlab/xr_interface.py b/xsimlab/drivers.py similarity index 100% rename from xsimlab/xr_interface.py rename to xsimlab/drivers.py diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 972b0c33..0915c5a3 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -8,7 +8,7 @@ from xarray import Dataset, register_dataset_accessor from .model import Model -from .xr_interface import XarraySimulationDriver +from .drivers import XarraySimulationDriver @register_dataset_accessor('filter') From 0ea32a56a972efe9c2aaa5d382317a6e94e48743 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 6 Apr 2018 15:16:19 +0200 Subject: [PATCH 52/97] (wip) create a 'stores' module and add InMemoryOutputStore --- xsimlab/drivers.py | 67 ++------------------------------------- xsimlab/stores.py | 71 ++++++++++++++++++++++++++++++++++++++++++ xsimlab/xr_accessor.py | 2 +- 3 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 xsimlab/stores.py diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index a6d0d672..477746b0 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -1,18 +1,6 @@ import copy import numpy as np -import xarray as xr - -from .utils import attr_fields_dict - - -def _get_dims_from_variable(array, variable): - """Given an array of values (snapshot) and a (xarray-simlab) Variable - object, Return dimension labels for the array.""" - for dims in variable.allowed_dims: - if len(dims) == array.ndim: - return dims - return tuple() class BaseSimulationDriver(object): @@ -93,7 +81,7 @@ def __init__(self, dataset, model, store, output_store): self.model = model super(XarraySimulationDriver, self).__init__(model, store, - output_store) + output_store) self.output_vars = dataset.xsimlab.output_vars self.output_save_steps = self._get_output_save_steps() @@ -160,57 +148,6 @@ def _maybe_save_output_vars(self, istep): if clock is None and self.snapshot_save[clock][istep]: self.update_output_store(var_keys) - def snapshot_to_xarray_variable(self, key, clock=None): - """Convert snapshots taken for a specific model variable to an - xarray.Variable object. - """ - p_name, var_name = key - p_obj = self.model[p_name] - variable = attr_fields_dict(p_obj)[var_name] - - array_list = self.output_store[key] - first_array = array_list[0] - - if len(array_list) == 1: - data = first_array - else: - data = np.stack(array_list) - - dims = _get_dims_from_variable(first_array, variable) - if clock is not None and len(array_list) > 1: - dims = (clock,) + dims - - attrs = variable.attrs.copy() - attrs['description'] = variable.description - - return xr.Variable(dims, data, attrs=attrs) - - def create_output_dataset(self): - """Build a new output Dataset from the input Dataset and - all snapshots taken during a model run. - """ - from .xr_accessor import SimlabAccessor - - xr_variables = {} - - for clock, vars in self.output_vars.items(): - for key in vars: - var_name = '__'.join(key) - xr_variables[var_name] = self.snapshot_to_xarray_variable( - key, clock=clock - ) - - out_ds = self.dataset.update(xr_variables, inplace=False) - - for clock in self.output_vars: - if clock is None: - attrs = out_ds.attrs - else: - attrs = out_ds[clock].attrs - attrs.pop(SimlabAccessor._output_vars_key) - - return out_ds - def run_model(self): """Run the model and return a new Dataset with all the simulation inputs and outputs. @@ -246,4 +183,4 @@ def run_model(self): self._maybe_save_output_vars(-1) self.model.finalize() - return self.create_output_dataset() + return self.output_store.to_dataset() diff --git a/xsimlab/stores.py b/xsimlab/stores.py new file mode 100644 index 00000000..7870473b --- /dev/null +++ b/xsimlab/stores.py @@ -0,0 +1,71 @@ +from collections import defaultdict + +import numpy as np +import xarray as xr + +from .utils import attr_fields_dict +from .xr_accessor import SimlabAccessor + + +def _get_dims_from_variable(array, variable): + """Given an array of values (snapshot) and a (xarray-simlab) Variable + object, Return dimension labels for the array.""" + for dims in variable.allowed_dims: + if len(dims) == array.ndim: + return dims + return tuple() + + +class InMemoryOutputStore(object): + + def __init__(self): + self._store = defaultdict(list) + + def append(self): + pass + + def _snapshot_to_xarray_variable(self, key, clock=None): + """Convert snapshots taken for a specific model variable to an + xarray.Variable object. + """ + p_name, var_name = key + p_obj = self.model[p_name] + variable = attr_fields_dict(p_obj)[var_name] + + array_list = self.output_store[key] + first_array = array_list[0] + + if len(array_list) == 1: + data = first_array + else: + data = np.stack(array_list) + + dims = _get_dims_from_variable(first_array, variable) + if clock is not None and len(array_list) > 1: + dims = (clock,) + dims + + attrs = variable.attrs.copy() + attrs['description'] = variable.description + + return xr.Variable(dims, data, attrs=attrs) + + def to_dataset(self): + xr_variables = {} + + for clock, vars in self.output_vars.items(): + for key in vars: + var_name = '__'.join(key) + xr_variables[var_name] = self._snapshot_to_xarray_variable( + key, clock=clock + ) + + out_ds = self.dataset.update(xr_variables, inplace=False) + + for clock in self.output_vars: + if clock is None: + attrs = out_ds.attrs + else: + attrs = out_ds[clock].attrs + attrs.pop(SimlabAccessor._output_vars_key) + + return out_ds diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 0915c5a3..197f8333 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -454,7 +454,7 @@ def run(self, model=None, safe_mode=True): output_store = defaultdict(list) sim_driver = XarraySimulationDriver(model, self._ds, - store, output_store) + store, output_store) out_ds = sim_driver.run_model() From 40337cc07978aa51b42d5f90d876f634342a31f2 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 16 Apr 2018 13:50:47 +0200 Subject: [PATCH 53/97] fully implement xarray simulation driver and in-memory output store --- xsimlab/__init__.py | 2 +- xsimlab/drivers.py | 84 +++++++++++++++++++++++++++++++++--------- xsimlab/stores.py | 70 +++++------------------------------ xsimlab/xr_accessor.py | 27 +++++++++++--- 4 files changed, 98 insertions(+), 85 deletions(-) diff --git a/xsimlab/__init__.py b/xsimlab/__init__.py index 0d9df25c..ab453f7e 100644 --- a/xsimlab/__init__.py +++ b/xsimlab/__init__.py @@ -2,7 +2,7 @@ xarray-simlab. """ -#from .xr_accessor import SimlabAccessor, create_setup +from .xr_accessor import SimlabAccessor, create_setup from .variable import variable, on_demand, foreign, group from .process import filter_variables, process, process_info, variable_info from .model import Model diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index 477746b0..53e9956a 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -1,6 +1,9 @@ import copy import numpy as np +import xarray as xr + +from .utils import attr_fields_dict class BaseSimulationDriver(object): @@ -12,7 +15,6 @@ class BaseSimulationDriver(object): simulation output store. """ - def __init__(self, model, store, output_store): self.model = model self.store = store @@ -62,26 +64,40 @@ def update_output_store(self, output_var_keys): def run_model(self): """Main function of the driver used to run a simulation (must be - implemented in subclasses). + implemented in sub-classes). """ raise NotImplementedError() +def _get_dims_from_variable(array, variable): + """Given an array with numpy compatible interface and a + (xarray-simlab) variable, return dimension labels for the + array. + + """ + for dims in variable.dims: + if len(dims) == array.ndim: + return dims + + return tuple() + + class XarraySimulationDriver(BaseSimulationDriver): """Simulation driver using xarray.Dataset objects as I/O. - - Performs some sanity checks on the content of the given input Dataset. - - Sets model inputs from the input Dataset. - - Saves model outputs for given model variables (defined in attributes of - Dataset) following one or several clocks (i.e., Dataset coordinates). - - Gets simulation results as a new xarray.Dataset object. + - Perform some sanity checks on the content of the given input Dataset. + - Set model inputs from data variables or coordinates in the input Dataset. + - Save model outputs for given model variables, defined in specific + attributes of the input Dataset, on time frequencies given by clocks + defined as coordinates in the input Dataset. + - Get simulation results as a new xarray.Dataset object. """ def __init__(self, dataset, model, store, output_store): self.model = model - super(XarraySimulationDriver, self).__init__(model, store, - output_store) + super(XarraySimulationDriver, self).__init__( + model, store, output_store) self.output_vars = dataset.xsimlab.output_vars self.output_save_steps = self._get_output_save_steps() @@ -93,20 +109,20 @@ def __init__(self, dataset, model, store, output_store): self._check_missing_model_inputs() def _check_missing_model_inputs(self): - """Check if all model inputs have their corresponding data variables + """Check if all model inputs have their corresponding variables in the input Dataset. """ - missing_data_vars = [] + missing_xr_vars = [] for p_name, var_name in self.model.input_vars: xr_var_name = p_name + '__' + var_name - if xr_var_name not in self.dataset.data_vars: - missing_data_vars.append(xr_var_name) + if xr_var_name not in self.dataset: + missing_xr_vars.append(xr_var_name) - if missing_data_vars: - raise KeyError("Missing data variables %s in Dataset" - % missing_data_vars) + if missing_xr_vars: + raise KeyError("Missing variables %s in Dataset" + % missing_xr_vars) def _get_output_save_steps(self): """Returns a dictionary where keys are names of clock coordinates and @@ -145,9 +161,41 @@ def _maybe_save_output_vars(self, istep): else: for clock, var_keys in self.output_vars.items(): - if clock is None and self.snapshot_save[clock][istep]: + if clock is not None and self.snapshot_save[clock][istep]: self.update_output_store(var_keys) + def _to_xr_variable(self, key, clock): + """Convert an output variable to a xarray.Variable object.""" + p_name, var_name = key + p_obj = self.model[p_name] + var = attr_fields_dict(p_obj)[var_name] + + data = self.output_store[key] + if clock is None: + data = data[0] + + dims = _get_dims_from_variable(data, var) + if clock is not None: + dims = (clock,) + dims + + attrs = var.metadata['attrs'].copy() + attrs['description'] = var.metadata['description'] + + return xr.Variable(dims, data, attrs=attrs) + + def _get_output_dataset(self): + """Return a new dataset as a copy of the input dataset updated with + output variables. + """ + xr_vars = {} + + for clock, vars in self.output_vars.items(): + for key in vars: + var_name = '__'.join(key) + xr_vars[var_name] = self._to_xr_variable(key, clock) + + return self.dataset.update(xr_vars, inplace=False) + def run_model(self): """Run the model and return a new Dataset with all the simulation inputs and outputs. @@ -183,4 +231,4 @@ def run_model(self): self._maybe_save_output_vars(-1) self.model.finalize() - return self.output_store.to_dataset() + return self._get_output_dataset() diff --git a/xsimlab/stores.py b/xsimlab/stores.py index 7870473b..0b143793 100644 --- a/xsimlab/stores.py +++ b/xsimlab/stores.py @@ -1,71 +1,21 @@ from collections import defaultdict +from copy import copy import numpy as np -import xarray as xr - -from .utils import attr_fields_dict -from .xr_accessor import SimlabAccessor - - -def _get_dims_from_variable(array, variable): - """Given an array of values (snapshot) and a (xarray-simlab) Variable - object, Return dimension labels for the array.""" - for dims in variable.allowed_dims: - if len(dims) == array.ndim: - return dims - return tuple() class InMemoryOutputStore(object): + """A simple, in-memory store for model outputs. + + It basically consists of a Python dictionary with lists as values, + which are converted to numpy arrays on store read-access. + """ def __init__(self): self._store = defaultdict(list) - def append(self): - pass - - def _snapshot_to_xarray_variable(self, key, clock=None): - """Convert snapshots taken for a specific model variable to an - xarray.Variable object. - """ - p_name, var_name = key - p_obj = self.model[p_name] - variable = attr_fields_dict(p_obj)[var_name] - - array_list = self.output_store[key] - first_array = array_list[0] - - if len(array_list) == 1: - data = first_array - else: - data = np.stack(array_list) - - dims = _get_dims_from_variable(first_array, variable) - if clock is not None and len(array_list) > 1: - dims = (clock,) + dims - - attrs = variable.attrs.copy() - attrs['description'] = variable.description - - return xr.Variable(dims, data, attrs=attrs) - - def to_dataset(self): - xr_variables = {} - - for clock, vars in self.output_vars.items(): - for key in vars: - var_name = '__'.join(key) - xr_variables[var_name] = self._snapshot_to_xarray_variable( - key, clock=clock - ) - - out_ds = self.dataset.update(xr_variables, inplace=False) - - for clock in self.output_vars: - if clock is None: - attrs = out_ds.attrs - else: - attrs = out_ds[clock].attrs - attrs.pop(SimlabAccessor._output_vars_key) + def append(self, key, value): + self._store[key].append(copy(value)) - return out_ds + def __getitem__(self, key): + return np.array(self._store[key]) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 197f8333..ac531969 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -45,9 +45,9 @@ def _maybe_get_model_from_context(model): class SimlabAccessor(object): """simlab extension to :class:`xarray.Dataset`.""" - _clock_key = '_xsimlab_snapshot_clock' - _master_clock_key = '_xsimlab_master_clock' - _output_vars_key = '_xsimlab_output_vars' + _clock_key = '__xsimlab_snapshot_clock__' + _master_clock_key = '__xsimlab_master_clock__' + _output_vars_key = '__xsimlab_output_vars__' def __init__(self, ds): self._ds = ds @@ -255,9 +255,11 @@ def output_vars(self): tuples - as values. """ output_vars = {} + for cname, coord in self._ds.coords.items(): output_vars.update(self._get_output_vars(cname, coord)) output_vars.update(self._get_output_vars(None, self._ds)) + return output_vars def update_clocks(self, model=None, clocks=None, master_clock=None): @@ -427,6 +429,20 @@ def filter_vars(self, model=None): return ds + def _clean_output_dataset(self, ds): + """Return a new dataset after having removed unnecessary attributes.""" + clean_ds = ds.copy() + + for clock in clean_ds.output_vars: + if clock is None: + attrs = clean_ds.attrs + else: + attrs = clean_ds[clock].attrs + + attrs.pop(self._output_vars_key) + + return clean_ds + def run(self, model=None, safe_mode=True): """Run the model. @@ -453,10 +469,9 @@ def run(self, model=None, safe_mode=True): store = {} output_store = defaultdict(list) - sim_driver = XarraySimulationDriver(model, self._ds, - store, output_store) + driver = XarraySimulationDriver(model, self._ds, store, output_store) - out_ds = sim_driver.run_model() + out_ds = driver.run_model().pipe(self._clean_output_dataset) return out_ds From 6de2a82684aa4b5463ac50e2bdd394125a2f398c Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 16 Apr 2018 20:42:13 +0200 Subject: [PATCH 54/97] add Model.all_vars and Model.all_vars_dict properties --- doc/whats_new.rst | 3 +++ xsimlab/model.py | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index d4fb8f8b..0b34080b 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -98,6 +98,9 @@ Enhancements data storage backends (e.g., on-disk, distributed). - Added ``Model.dependent_processes`` property (so far this was not in public API). +- Added ``Model.all_vars`` and ``Model.all_vars_dict`` properties that + are similar to ``Model.input_vars`` and ``Model.input_vars_dict`` + but return all variable names in the model. Regressions (will be fixed in future releases) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/xsimlab/model.py b/xsimlab/model.py index b89dfbc9..3cbf3498 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -4,7 +4,7 @@ from .variable import VarIntent, VarType from .process import (ensure_process_decorated, filter_variables, get_target_variable) -from .utils import AttrMapping, ContextMixin, has_method +from .utils import AttrMapping, ContextMixin, has_method, attr_fields_dict from .formatting import repr_model @@ -129,6 +129,19 @@ def set_process_keys(self): if od_key is not None: p_obj.__xsimlab_od_keys__[var.name] = od_key + def get_all_variables(self): + """Get all variables in the model as a list of + ``(process_name, var_name)`` tuples. + + """ + all_keys = [] + + for p_name, p_cls in self._processes_cls.items(): + all_keys += [(p_name, var_name) + for var_name in attr_fields_dict(p_cls)] + + return all_keys + def get_input_variables(self): """Get all input variables in the model as a list of ``(process_name, var_name)`` tuples. @@ -354,6 +367,9 @@ def __init__(self, processes): builder.bind_processes(self) builder.set_process_keys() + self._all_vars = builder.get_all_variables() + self._all_vars_dict = None + self._input_vars = builder.get_input_variables() self._input_vars_dict = None @@ -368,6 +384,30 @@ def __init__(self, processes): super(Model, self).__init__(self._processes) self._initialized = True + @property + def all_vars(self): + """Returns all variables in the model as a list of + ``(process_name, var_name)`` tuples (or an empty list). + + """ + return self._all_vars + + @property + def all_vars_dict(self): + """Returns all variables in the model as a dictionary of lists of + variable names grouped by process. + + """ + if self._all_vars_dict is None: + inputs = defaultdict(list) + + for p_name, var_name in self._all_vars: + inputs[p_name].append(var_name) + + self._all_vars_dict = dict(inputs) + + return self._all_vars_dict + @property def input_vars(self): """Returns all variables that require setting a value before running @@ -391,8 +431,8 @@ def input_vars_dict(self): if self._input_vars_dict is None: inputs = defaultdict(list) - for proc_name, var_name in self._input_vars: - inputs[proc_name].append(var_name) + for p_name, var_name in self._input_vars: + inputs[p_name].append(var_name) self._input_vars_dict = dict(inputs) From f8ce6484fc098255b054b4f42cc2a0b3754bd905 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 16 Apr 2018 20:42:48 +0200 Subject: [PATCH 55/97] misc fixes --- xsimlab/drivers.py | 2 +- xsimlab/utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index 53e9956a..6a97e814 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -168,7 +168,7 @@ def _to_xr_variable(self, key, clock): """Convert an output variable to a xarray.Variable object.""" p_name, var_name = key p_obj = self.model[p_name] - var = attr_fields_dict(p_obj)[var_name] + var = attr_fields_dict(type(p_obj))[var_name] data = self.output_store[key] if clock is None: diff --git a/xsimlab/utils.py b/xsimlab/utils.py index c2e2324b..0b6a6c8c 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -5,7 +5,6 @@ import threading from collections import (Mapping, KeysView, ItemsView, ValuesView, OrderedDict) -from functools import wraps from contextlib import suppress from importlib import import_module from inspect import isclass From 81ca36af286b280f5264b9ba4c4bf426ee6fc1dc Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 16 Apr 2018 20:43:04 +0200 Subject: [PATCH 56/97] update xarray SimlabAccessor --- doc/whats_new.rst | 6 + xsimlab/xr_accessor.py | 313 +++++++++++++++++++++++++---------------- 2 files changed, 196 insertions(+), 123 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 0b34080b..7738d3a3 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -77,6 +77,10 @@ changes are effective now! - For simplicity, ``Dataset.xsimlab.snapshot_vars`` has been renamed to ``output_vars``. The corresponding arguments in ``create_setup`` and ``Dataset.xsimlab.update_vars`` have been renamed accordingly. +- Values for all model inputs must be provided when creating or + updating a setup using ``create_setup`` or + ``Dataset.xsimlab.update_vars``. this is a regression that will be + fixed in the next releases. Enhancements ~~~~~~~~~~~~ @@ -101,6 +105,8 @@ Enhancements - Added ``Model.all_vars`` and ``Model.all_vars_dict`` properties that are similar to ``Model.input_vars`` and ``Model.input_vars_dict`` but return all variable names in the model. +- ``input_vars`` and ``output_vars`` arguments of ``create_setup`` and + ``Dataset.xsimlab.update_vars`` now accepts different formats. Regressions (will be fixed in future releases) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index ac531969..302d43c9 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -5,10 +5,11 @@ from collections import defaultdict import numpy as np -from xarray import Dataset, register_dataset_accessor +from xarray import as_variable, Dataset, register_dataset_accessor -from .model import Model from .drivers import XarraySimulationDriver +from .model import Model +from .utils import attr_fields_dict @register_dataset_accessor('filter') @@ -33,7 +34,7 @@ def _maybe_get_model_from_context(model): try: return Model.get_context() except TypeError: - raise TypeError("no model found in context") + raise TypeError("No model found in context") if not isinstance(model, Model): raise TypeError("%s is not an instance of xsimlab.Model" % model) @@ -41,11 +42,89 @@ def _maybe_get_model_from_context(model): return model +def as_variable_key(key): + """Returns ``key`` as a tuple of the form + ``('process_name', 'var_name')``. + + If ``key`` is given as a string, then process name and variable + name must be separated unambiguously by '__' (double underscore) + and must not be empty. + + """ + key_tuple = None + + if isinstance(key, tuple) and len(key) == 2: + key_tuple = key + + elif isinstance(key, str): + key_split = key.split('__') + if len(key_split) == 2: + p_name, var_name = key_split + if p_name and var_name: + key_tuple = (p_name, var_name) + + if key_tuple is None: + raise ValueError("{!r} is not a valid input variable key".format(key)) + + return key_tuple + + +def _flatten_inputs(input_vars): + """Returns ``input_vars`` as a flat dictionary where keys are tuples in + the form ``(process_name, var_name)``. Raises an error if the + given format appears to be invalid. + + """ + flatten_vars = {} + + for key, val in input_vars.items(): + if isinstance(key, str) and isinstance(val, dict): + for var_name, var_value in val.items(): + flatten_vars[(key, var_name)] = var_value + + else: + flatten_vars[as_variable_key(key)] = val + + return flatten_vars + + +def _flatten_outputs(output_vars): + """Returns ``output_vars`` as a flat dictionary where keys are clock + names (or None) and values are lists of tuples in the form + ``(process_name, var_name)``. + + """ + flatten_vars = {} + + for clock, out_vars in output_vars.items(): + if isinstance(out_vars, dict): + var_list = [] + for p_name, var_names in out_vars.items(): + if isinstance(var_names, str): + var_list.append((p_name, var_names)) + else: + var_list += [(p_name, vname) for vname in var_names] + + elif isinstance(out_vars, [tuple, str]): + var_list = [as_variable_key(out_vars)] + + elif isinstance(out_vars, list): + var_list = [as_variable_key(k) for k in out_vars] + + else: + raise ValueError("Cannot interpret {!r} as valid output " + "variable key(s)".format(out_vars)) + + flatten_vars[clock] = var_list + + return flatten_vars + + @register_dataset_accessor('xsimlab') class SimlabAccessor(object): """simlab extension to :class:`xarray.Dataset`.""" - _clock_key = '__xsimlab_snapshot_clock__' + _clock_key = '__xsimlab_output_clock__' _master_clock_key = '__xsimlab_master_clock__' _output_vars_key = '__xsimlab_output_vars__' @@ -156,6 +235,7 @@ def _set_snapshot_clock(self, dim, data=None, start=0., end=None, da_snapshot_clock = da_master_clock.sel(**kwargs) self._ds[dim] = da_snapshot_clock.rename({self.master_clock_dim: dim}) + # .sel copies variable attributes self._ds[dim].attrs.pop(self._master_clock_key) @@ -164,87 +244,60 @@ def _set_snapshot_clock(self, dim, data=None, start=0., end=None, if attr_value is not None: self._ds[dim].attrs[attr_name] = attr_value - def _set_input_vars(self, model, process, **inputs): - if isinstance(process, Process): - process = process.name - if process not in model: - raise KeyError("no process named %r found in current model" - % process) + def _set_input_vars(self, model, input_vars): + invalid_inputs = set(input_vars) - set(model.input_vars) + if invalid_inputs: + raise KeyError( + "{} is/are not valid key(s) for input variables in model {}" + .format(', '.join([k for k in invalid_inputs]), model) + ) - process_inputs = model.input_vars[process] + missing_inputs = set(model.input_vars) - set(input_vars) + if missing_inputs: + raise KeyError( + "Missing value for input variable(s) {}" + .format(', '.join([k for k in missing_inputs])) + ) + + for (p_name, var_name), data in input_vars.items(): + p_obj = model[p_name] + var = attr_fields_dict(type(p_obj))[var_name] + + xr_var_name = p_name + '__' + var_name + xr_var = as_variable(data) + + xr_var.attrs.update(var.metadata['attrs']) + if var.metadata['description']: + xr_var.attrs['description'] = var.metadata['description'] - invalid_inputs = set(inputs) - set(process_inputs) - if invalid_inputs: - raise ValueError("%s are not valid input variables of %r" - % (', '.join([name for name in invalid_inputs]), - process)) - - # convert to xarray variables and validate the given dimensions - xr_variables = {} - for name, var in model.input_vars[process].items(): - xr_var = var.to_xarray_variable(inputs.get(name)) - var.validate_dimensions(xr_var.dims, - ignore_dims=(self.master_clock_dim, - 'this_variable')) - xr_variables[name] = xr_var - - # validate at the process level - # first assign values to a cloned process object to avoid conflicts - process_obj = model._processes[process].clone() - for name, xr_var in xr_variables.items(): - process_obj[name].value = xr_var.values - process_obj.validate() - - # maybe set optional variables, and validate each variable - for name, xr_var in xr_variables.items(): - var = process_obj[name] - if var.value is not xr_var.values: - xr_var = var.to_xarray_variable(var.value) - xr_variables[name] = xr_var - var.run_validators(xr_var) - var.validate(xr_var) - - # add variables to dataset if all validation tests passed - # also rename the 'this_variable' dimension if present - for name, xr_var in xr_variables.items(): - xr_var_name = process + '__' + name - rename_dict = {'this_variable': xr_var_name} - dims = tuple(rename_dict.get(dim, dim) for dim in xr_var.dims) - xr_var.dims = dims self._ds[xr_var_name] = xr_var - def _set_output_vars(self, model, clock_dim, **process_vars): - xr_vars_list = [] - - for proc_name, vars in sorted(process_vars.items()): - if proc_name not in model: - raise KeyError("no process named %r found in current model" - % proc_name) - process = model[proc_name] - if isinstance(vars, str): - vars = [vars] - for var_name in vars: - if process.variables.get(var_name, None) is None: - raise KeyError("process %r has no variable %r" - % (proc_name, var_name)) - xr_vars_list.append(proc_name + '__' + var_name) - - output_vars = ','.join(xr_vars_list) - - if clock_dim is None: + def _set_output_vars(self, model, clock, output_vars): + invalid_outputs = set(output_vars) - set(model.all_vars) + if invalid_outputs: + raise KeyError( + "{} is/are not valid key(s) for variables in model {}" + .format(', '.join([k for k in invalid_outputs]), model) + ) + + output_vars = ','.join([p_name + '__' + var_name + for (p_name, var_name) in output_vars]) + + if clock is None: self._ds.attrs[self._output_vars_key] = output_vars + else: - if clock_dim not in self.clock_coords: - raise ValueError("%r coordinate is not a valid clock " - "coordinate. " % clock_dim) - coord = self.clock_coords[clock_dim] + if clock not in self.clock_coords: + raise ValueError("{!r} coordinate is not a valid clock " + "coordinate.".format(clock)) + coord = self.clock_coords[clock] coord.attrs[self._output_vars_key] = output_vars - def _get_output_vars(self, name, obj): - vars_str = obj.attrs.get(self._output_vars_key, '') - if vars_str: - return {name: [tuple(s.split('__')) - for s in vars_str.split(',')]} + def _get_output_vars(self, clock, ds_or_coord): + out_attr = ds_or_coord.attrs.get(self._output_vars_key, '') + + if out_attr: + return {clock: [as_variable_key(k) for k in out_attr.split(',')]} else: return {} @@ -256,8 +309,9 @@ def output_vars(self): """ output_vars = {} - for cname, coord in self._ds.coords.items(): - output_vars.update(self._get_output_vars(cname, coord)) + for clock, clock_coord in self.clock_coords.items(): + output_vars.update(self._get_output_vars(clock, clock_coord)) + output_vars.update(self._get_output_vars(None, self._ds)) return output_vars @@ -326,8 +380,8 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): for dim, var_list in self.output_vars.items(): var_dict = defaultdict(list) - for proc_name, var_name in var_list: - var_dict[proc_name].append(var_name) + for p_name, var_name in var_list: + var_dict[p_name].append(var_name) if dim is None or dim in ds: ds.xsimlab._set_output_vars(model, dim, **var_dict) @@ -337,9 +391,6 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): def update_vars(self, model=None, input_vars=None, output_vars=None): """Update model input values and/or output variable names. - Add or replace all input values (resp. output variable names) per - given process (resp. clock coordinate). - More details about the values allowed for the parameters below can be found in the doc of :meth:`xsimlab.create_setup`. @@ -347,9 +398,10 @@ def update_vars(self, model=None, input_vars=None, output_vars=None): ---------- model : :class:`xsimlab.Model` object, optional Reference model. If None, tries to get model from context. - input_vars : dict of dicts, optional - Model input values given per process. - output_vars : dict of dicts, optional + input_vars : dict, optional + Model input values (may be grouped per process name, as dict of + dicts). + output_vars : dict, optional Model variables to save as simulation output, given per clock coordinate. @@ -369,21 +421,23 @@ def update_vars(self, model=None, input_vars=None, output_vars=None): ds = self._ds.copy() if input_vars is not None: - for proc_name, vars in input_vars.items(): - ds.xsimlab._set_input_vars(model, proc_name, **vars) + ds.xsimlab._set_input_vars(model, _flatten_inputs(input_vars)) if output_vars is not None: - for dim, proc_vars in output_vars.items(): - ds.xsimlab._set_output_vars(model, dim, **proc_vars) + for clock, out_vars in output_vars.items(): + ds.xsimlab._set_output_vars(model, clock, + _flatten_outputs(out_vars)) return ds def filter_vars(self, model=None): """Filter Dataset content according to Model. - Keep only data variables and coordinates that correspond to inputs of - the model (keep clock coordinates too). Also update snapshot-specific - attributes so that their values all correspond to processes and + Keep only data variables and coordinates that correspond to + inputs of the model (keep clock coordinates too). + + Also update xsimlab-specific attributes so that output + variables given per clock only refer to processes and variables defined in the model. Parameters @@ -404,28 +458,27 @@ def filter_vars(self, model=None): """ model = _maybe_get_model_from_context(model) + # drop variables drop_variables = [] for xr_var_name in self._ds: if xr_var_name in self.clock_coords: continue + try: - proc_name, var_name = xr_var_name.split('__') + p_name, var_name = xr_var_name.split('__') except ValueError: continue - if not model.is_input((proc_name, var_name)): + if (p_name, var_name) not in model.input_vars: drop_variables.append(xr_var_name) ds = self._ds.drop(drop_variables) - for dim, var_list in self.output_vars.items(): - var_dict = defaultdict(list) - for proc_name, var_name in var_list: - if model.get(proc_name, {}).get(var_name, False): - var_dict[proc_name].append(var_name) - - ds.xsimlab._set_output_vars(model, dim, **var_dict) + # update output variable attributes + for clock, out_vars in self.output_vars.items(): + new_out_vars = [key for key in out_vars if key in model.all_vars] + ds.xsimlab._set_output_vars(model, clock, new_out_vars) return ds @@ -493,9 +546,10 @@ def create_setup(model=None, clocks=None, master_clock=None, input_vars=None, output_vars=None): """Create a specific setup for model runs. - This convenient function creates a new :class:`xarray.Dataset` object with - model input values, time steps and model output variables (including - snapshot times) as data variables, coordinates and attributes. + This convenient function creates a new :class:`xarray.Dataset` + object with everything needed to run a model (i.e., input values, + time steps, output variables to save at given times) as data + variables, coordinates and attributes. Parameters ---------- @@ -513,22 +567,39 @@ def create_setup(model=None, clocks=None, master_clock=None, A dictionary with at least a 'dim' key can be provided instead, it allows setting time units and calendar (CF-conventions) with 'units' and 'calendar' keys. - input_vars : dict of dicts, optional - Model inputs values given per process. The structure of the dict of - dicts looks like ``{'process_name': {'var_name': value, ...}, ...}``. + input_vars : dict, optional + Dictionary with values given for model inputs. Entries of the + dictionary may look like: + + - ``'foo': {'bar': value, ...}`` or + - ``('foo', 'bar'): value`` or + - ``'foo__bar': value`` + + where ``foo`` is the name of a existing process in the model and + ``bar`` is the name of an (input) variable declared in that process. + Values are anything that can be easily converted to :class:`xarray.Variable` objects, e.g., single values, array-like, - (dims, data, attrs) tuples or xarray objects. - output_vars : dict of dicts, optional - Model variables to save as simulation output, given per clock - coordinate. The structure of the dict of dicts looks like - ``{'dim': {'process_name': 'var_name', ...}, ...}``. + ``(dims, data, attrs)`` tuples or xarray objects. + output_vars : dict, optional + Dictionary with model variable names to save as simulation output, + given per clock coordinate. Entries of the given dictionary may + look like: + + - ``'dim': {'foo': 'bar'}`` or + - ``'dim': {'foo': ('bar', 'baz')}`` or + - ``'dim': ('foo', 'bar')`` or + - ``'dim': [('foo', 'bar'), ('foo', 'baz')]`` or + - ``'dim': 'foo__bar'`` or + - ``'dim': ['foo__bar', 'foo__baz']`` + + where ``foo`` is the name of a existing process in the model and + ``bar``, ``baz`` are the names of variables declared in that process. + If ``'dim'`` corresponds to the dimension of a clock coordinate, - snapshot values will be recorded at each time given by the coordinate - labels. if None is given, only one value will be recorded at the + new output values will be saved at each time given by the coordinate + labels. if None is given instead, only one value will be saved at the end of the simulation. - Note that instead of ``'var_name'``, a tuple of multiple variable names - (declared in the same process) can be given. Returns ------- @@ -536,7 +607,7 @@ def create_setup(model=None, clocks=None, master_clock=None, A new Dataset object with model inputs as data variables or coordinates (depending on their given value) and clock coordinates. The names of the input variables also include the name of their process - (i.e., 'process_name__var_name'). + (i.e., 'foo__bar'). Notes ----- @@ -561,10 +632,6 @@ def create_setup(model=None, clocks=None, master_clock=None, with the labels of the master clock coordinate. Otherwise raise a KeyError if labels are not valid. (DataArray.sel is used internally). - Inputs of ``model`` for which no value is given are still added as variables - in the returned Dataset, using their default value (if any). It requires - that their process are provided as keys of ``input_vars``, though. - Output variable names are added in Dataset as specific attributes (global and/or clock coordinate attributes). From 9c200a82ec201b30b914c0e0113a4f7f76786e96 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 16 Apr 2018 20:50:09 +0200 Subject: [PATCH 57/97] use InMemoryOutputStore --- xsimlab/xr_accessor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 302d43c9..3fe3bfee 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -9,6 +9,7 @@ from .drivers import XarraySimulationDriver from .model import Model +from .stores import InMemoryOutputStore from .utils import attr_fields_dict @@ -520,7 +521,7 @@ def run(self, model=None, safe_mode=True): model = model.clone() store = {} - output_store = defaultdict(list) + output_store = InMemoryOutputStore() driver = XarraySimulationDriver(model, self._ds, store, output_store) From c89a6b443290683dc5d7686a25bd73c6521a2e66 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 18 Apr 2018 11:59:40 +0200 Subject: [PATCH 58/97] ensure invalid dims are ordered by ndim in error msg --- xsimlab/variable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index ee492489..b0c2a46c 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -48,15 +48,17 @@ def _as_dim_tuple(dims): else: dims = [dims] + # check ndim uniqueness could be simpler but provides detailed error msg + fget_ndim = lambda dims: len(dims) + dims_sorted = sorted(dims, key=fget_ndim) ndim_groups = [list(g) - for _, g in itertools.groupby(dims, lambda d: len(d))] + for _, g in itertools.groupby(dims_sorted, fget_ndim)] if len(ndim_groups) != len(dims): invalid_dims = [g for g in ndim_groups if len(g) > 1] invalid_msg = ' and '.join( ', '.join(str(d) for d in group) for group in invalid_dims ) - raise ValueError("the following combinations of dimension labels " "are ambiguous for a variable: {}" .format(invalid_msg)) From 037d3bc6ccbc69bc87d0a17470e6373b6513b489 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 18 Apr 2018 16:16:17 +0200 Subject: [PATCH 59/97] improve some error messages --- xsimlab/process.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 89f62a4a..e56c7fd6 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -214,22 +214,21 @@ def put_in_store(self, value): target_str = target_var.name if target_type == VarType.GROUP: - raise ValueError("Variable '{var}' links to group variable '{target}', " - "which is not supported. Declare {var} as a group " + raise ValueError("Variable {var!r} links to group variable {target!r}, " + "which is not supported. Declare {var!r} as a group " "variable instead." .format(var=var.name, target=target_str)) elif (var_type == VarType.FOREIGN and var_intent == VarIntent.OUT and target_intent == VarIntent.OUT): - raise ValueError("Incompatible intent given for variables " - "'{}' ('{}') and '{}' ('{}')" - .format(var.name, var_intent.value, - target_str, target_intent.value)) + raise ValueError("Conflict between foreign variable {!r} and its " + "target variable {!r}, both have intent 'out'." + .format(var.name, target_str)) elif target_type == VarType.ON_DEMAND: if var_intent != VarIntent.IN: - raise ValueError("Variable '{}' targeting on-demand variable " - "'{}' should have intent='in' (found '{}')" + raise ValueError("Variable {!r} targeting on-demand variable " + "{!r} should have intent='in' (found {!r})" .format(var.name, target_str, var_intent.value)) return property(fget=get_on_demand, doc=var_doc) From 944b5ec2d3da85c57cea9c618e877cbb0e1cabde Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 18 Apr 2018 16:35:07 +0200 Subject: [PATCH 60/97] intent=inout not supported by foreign (ambiguous process ordering) --- xsimlab/variable.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/xsimlab/variable.py b/xsimlab/variable.py index b0c2a46c..99062918 100644 --- a/xsimlab/variable.py +++ b/xsimlab/variable.py @@ -203,18 +203,28 @@ def foreign(other_process_cls, var_name, intent='in'): Class in which the variable is defined. var_name : str Name of the corresponding variable declared in `other_process_cls`. - intent : {'in', 'out', 'inout'}, optional + intent : {'in', 'out'}, optional Defines whether the foreign variable is an input (i.e., the process needs the variable's value for its computation), an output (i.e., the - process computes a value for the variable) or both an input/output - (i.e., the process may update the value of the variable). + process computes a value for the variable). (default: input). See Also -------- :func:`variable` + Notes + ----- + Unlike for :func:`variable`, ``intent='inout'`` is not supported + here (i.e., the process may not update the value of a foreign + variable) as it would result in ambiguous process ordering in a + model. + """ + if intent == 'inout': + raise ValueError("intent='inout' is not supported for " + "foreign variables") + description = ("Reference to variable {!r} " "defined in class {!r}" .format(var_name, other_process_cls.__name__)) From 341f273fce2afee36733596449d0423a201f8249 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 18 Apr 2018 17:15:19 +0200 Subject: [PATCH 61/97] ignore attr.Attribute objects that are not xsimlab-specific --- xsimlab/dot.py | 8 ++++---- xsimlab/drivers.py | 4 ++-- xsimlab/formatting.py | 6 +++--- xsimlab/model.py | 4 ++-- xsimlab/process.py | 25 +++++++++++++------------ xsimlab/utils.py | 10 ++++++++++ xsimlab/xr_accessor.py | 4 ++-- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/xsimlab/dot.py b/xsimlab/dot.py index 1d6241e4..4c5a1dd0 100644 --- a/xsimlab/dot.py +++ b/xsimlab/dot.py @@ -12,7 +12,7 @@ import os from functools import partial -from .utils import attr_fields_dict, import_required, maybe_to_list +from .utils import variables_dict, import_required, maybe_to_list from .variable import VarIntent, VarType @@ -96,7 +96,7 @@ def _add_var(self, var, p_name): def add_inputs(self): for p_name, var_name in self.model._input_vars: p_cls = type(self.model[p_name]) - var = attr_fields_dict(p_cls)[var_name] + var = variables_dict(p_cls)[var_name] self._add_var(var, p_name) @@ -104,7 +104,7 @@ def add_variables(self): for p_name, p_obj in self.model._processes.items(): p_cls = type(p_obj) - for var_name, var in attr_fields_dict(p_cls).items(): + for var_name, var in variables_dict(p_cls).items(): self._add_var(var, p_name) def add_var_and_targets(self, p_name, var_name): @@ -117,7 +117,7 @@ def add_var_and_targets(self, p_name, var_name): for p_name, p_obj in self.model._processes.items(): p_cls = type(p_obj) - for var_name, var in attr_fields_dict(p_cls).items(): + for var_name, var in variables_dict(p_cls).items(): target_keys = _get_target_keys(p_obj, var_name) if ((p_name, var_name) == (this_p_name, this_var_name) or diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index 6a97e814..726cef6c 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -3,7 +3,7 @@ import numpy as np import xarray as xr -from .utils import attr_fields_dict +from .utils import variables_dict class BaseSimulationDriver(object): @@ -168,7 +168,7 @@ def _to_xr_variable(self, key, clock): """Convert an output variable to a xarray.Variable object.""" p_name, var_name = key p_obj = self.model[p_name] - var = attr_fields_dict(type(p_obj))[var_name] + var = variables_dict(type(p_obj))[var_name] data = self.output_store[key] if clock is None: diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 8af82af4..04afb89a 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -1,7 +1,7 @@ """Formatting utils and functions.""" import textwrap -from .utils import attr_fields_dict, has_method +from .utils import variables_dict, has_method from .variable import VarIntent, VarType @@ -105,7 +105,7 @@ def repr_process(process): header = "<{} {} (xsimlab process)>".format(process_cls.__name__, process_name) - variables = attr_fields_dict(process_cls) + variables = variables_dict(process_cls) col_width = _calculate_col_width(variables) @@ -155,7 +155,7 @@ def repr_model(model): input_var_lines = [] for var_name in p_input_vars: - var = attr_fields_dict(type(p_obj))[var_name] + var = variables_dict(type(p_obj))[var_name] input_var_lines.append(_summarize_var(var, p_obj, col_width)) if input_var_lines: diff --git a/xsimlab/model.py b/xsimlab/model.py index 3cbf3498..5c87e3f2 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -4,7 +4,7 @@ from .variable import VarIntent, VarType from .process import (ensure_process_decorated, filter_variables, get_target_variable) -from .utils import AttrMapping, ContextMixin, has_method, attr_fields_dict +from .utils import AttrMapping, ContextMixin, has_method, variables_dict from .formatting import repr_model @@ -138,7 +138,7 @@ def get_all_variables(self): for p_name, p_cls in self._processes_cls.items(): all_keys += [(p_name, var_name) - for var_name in attr_fields_dict(p_cls)] + for var_name in variables_dict(p_cls)] return all_keys diff --git a/xsimlab/process.py b/xsimlab/process.py index e56c7fd6..71bf621e 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -5,7 +5,7 @@ from .variable import VarIntent, VarType from .formatting import repr_process, var_details -from .utils import attr_fields_dict +from .utils import variables_dict class NotAProcessClassError(ValueError): @@ -73,26 +73,27 @@ def filter_variables(process, var_type=None, intent=None, group=None, objects as values. """ - process = get_process_cls(process) + process_cls = get_process_cls(process) - fields = attr_fields_dict(process) + # be consistent and always return a dict (not OrderedDict) when no filter + vars = dict(variables_dict(process_cls)) if var_type is not None: - fields = {k: a for k, a in fields.items() - if a.metadata.get('var_type') == VarType(var_type)} + vars = {k: v for k, v in vars.items() + if v.metadata.get('var_type') == VarType(var_type)} if intent is not None: - fields = {k: a for k, a in fields.items() - if a.metadata.get('intent') == VarIntent(intent)} + vars = {k: v for k, v in vars.items() + if v.metadata.get('intent') == VarIntent(intent)} if group is not None: - fields = {k: a for k, a in fields.items() - if a.metadata.get('group') == group} + vars = {k: v for k, v in vars.items() + if v.metadata.get('group') == group} if func is not None: - fields = {k: a for k, a in fields.items() if func(a)} + vars = {k: v for k, v in vars.items() if func(v)} - return fields + return vars def get_target_variable(var): @@ -410,6 +411,6 @@ def variable_info(process, var_name, buf=None): buf = sys.stdout process = get_process_cls(process) - var = attr_fields_dict(process)[var_name] + var = variables_dict(process)[var_name] buf.write(var_details(var)) diff --git a/xsimlab/utils.py b/xsimlab/utils.py index 0b6a6c8c..566b1ab1 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -24,6 +24,16 @@ def attr_fields_dict(cls): return OrderedDict(((a.name, a) for a in attrs)) +def variables_dict(process_cls): + """Get all xsimlab variables declared in a process.""" + + # exclude attr.Attribute objects that are not xsimlab-specific + vars = OrderedDict((k, v) + for k, v in attr_fields_dict(process_cls).items() + if 'var_type' in v.metadata) + return vars + + def has_method(obj, meth): return callable(getattr(obj, meth, False)) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 3fe3bfee..66fd37aa 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -10,7 +10,7 @@ from .drivers import XarraySimulationDriver from .model import Model from .stores import InMemoryOutputStore -from .utils import attr_fields_dict +from .utils import variables_dict @register_dataset_accessor('filter') @@ -262,7 +262,7 @@ def _set_input_vars(self, model, input_vars): for (p_name, var_name), data in input_vars.items(): p_obj = model[p_name] - var = attr_fields_dict(type(p_obj))[var_name] + var = variables_dict(type(p_obj))[var_name] xr_var_name = p_name + '__' + var_name xr_var = as_variable(data) From 68acd3740e33ec5cc3fc9fdbf49b78f26e8cf705 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 18 Apr 2018 17:52:37 +0200 Subject: [PATCH 62/97] (wip) update tests --- xsimlab/tests/conftest.py | 201 ++++--------------------- xsimlab/tests/test_process.py | 196 ++++++++++++++++--------- xsimlab/tests/test_utils.py | 19 +++ xsimlab/tests/test_variable.py | 259 +++------------------------------ 4 files changed, 192 insertions(+), 483 deletions(-) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index bf4090ee..6d6035c6 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -1,187 +1,44 @@ -""" -This module provides a set of Process subclasses and pytest fixtures that are -used across the tests. -""" -from textwrap import dedent +"""This module provides a set of process classes and models as pytest +fixtures that are used across the tests. +""" +import attr import pytest -import numpy as np -import xarray as xr - -from xsimlab.variable.base import (Variable, ForeignVariable, VariableGroup, - VariableList, diagnostic) -from xsimlab.process import Process -from xsimlab.model import Model -from xsimlab.xr_accessor import SimlabAccessor -from xsimlab.xr_interface import DatasetModelInterface - - -class ExampleProcess(Process): - """A full example of process interface. - """ - var = Variable((), provided=True) - var_list = VariableList([Variable('x'), Variable([(), 'x'])]) - var_group = VariableGroup('group') - no_var = 'this is not a variable object' - class Meta: - time_dependent = False - - @diagnostic - def diag(self): - return 1 - - -@pytest.fixture -def process(): - return ExampleProcess() +import xsimlab as xs -@pytest.fixture(scope='session') -def process_repr(): - return dedent("""\ - Variables: - * diag DiagnosticVariable - * var Variable () - var_group VariableGroup 'group' - var_list VariableList - - Variable ('x') - - Variable (), ('x') - Meta: - time_dependent: False""") +@xs.process +class SomeProcess(object): + """Just used for foreign variable in ExampleProcess.""" + ref_var = xs.variable() -class Grid(Process): - x_size = Variable((), optional=True, description='grid size') - x = Variable('x', provided=True) +@xs.process +class OtherProcess(object): + """Just used for foreign variable in ExampleProcess.""" + ref_var = xs.variable() - class Meta: - time_dependent = False - def validate(self): - if np.asscalar(self.x_size.value) is None: - self.x_size.value = 5 +@xs.process +class ExampleProcess(object): + """A process with complete interface for testing.""" + in_var = xs.variable() + out_var = xs.variable(group='group1', intent='out') + inout_var = xs.variable(intent='inout') + in_foreign_var = xs.foreign(SomeProcess, 'ref_var') + out_foreign_var = xs.foreign(OtherProcess, 'ref_var', intent='out') + group_var = xs.group('group2') + od_var = xs.on_demand() - def initialize(self): - self.x.value = np.arange(self.x_size.value) + other_attrib = attr.attrib(init=False, cmp=False, repr=False) + other_attr = "this is not a xsimlab variable attribute" - -class Quantity(Process): - quantity = Variable('x', description='a quantity') - all_effects = VariableGroup('effect') - - def run_step(self, *args): - self.quantity.change = sum((var.value for var in self.all_effects)) - - def finalize_step(self): - self.quantity.state += self.quantity.change - - @diagnostic - def some_derived_quantity(self): - """some derived quantity.""" + @od_var.compute + def compute_od_var(self): return 1 - @diagnostic({'units': 'm'}) - def other_derived_quantity(self): - """other derived quantity.""" - return 2 - - -class SomeProcess(Process): - some_param = Variable((), default_value=1, description='some parameter') - x = ForeignVariable(Grid, 'x') - quantity = ForeignVariable(Quantity, 'quantity') - some_effect = Variable('x', group='effect', provided=True) - - # SomeProcess always appears before OtherProcess in a model - copy_param = Variable((), provided=True) - - def initialize(self): - self.copy_param.value = self.some_param.value - - def run_step(self, dt): - self.some_effect.value = self.x.value * self.some_param.value + dt - - def finalize(self): - self.some_effect.rate = 0 - - -class OtherProcess(Process): - x = ForeignVariable(Grid, 'x') - quantity = ForeignVariable(Quantity, 'quantity') - other_param = Variable((), default_value=1, description='other parameter') - other_effect = Variable('x', group='effect', provided=True) - - # OtherProcess should always appear after SomeProcess in a model - copy_param = ForeignVariable(SomeProcess, 'copy_param') - - def run_step(self, dt): - self.other_effect.value = self.x.value * self.copy_param.value - dt - - @diagnostic - def x2(self): - return self.x * 2 - - -class PlugProcess(Process): - meta_param = Variable(()) - some_param = ForeignVariable(SomeProcess, 'some_param', provided=True) - x = ForeignVariable(Grid, 'x') - - def run_step(self, *args): - self.some_param.value = self.meta_param.value - - -@pytest.fixture -def model(): - model = Model({'grid': Grid, - 'some_process': SomeProcess, - 'other_process': OtherProcess, - 'quantity': Quantity}) - return model - - -@pytest.fixture(scope='session') -def model_repr(): - return dedent("""\ - - grid - x_size (in) grid size - some_process - some_param (in) some parameter - other_process - other_param (in) other parameter - quantity - quantity (in) a quantity""") - @pytest.fixture -def input_dataset(): - clock_key = SimlabAccessor._clock_key - mclock_key = SimlabAccessor._master_clock_key - svars_key = SimlabAccessor._snapshot_vars_key - - ds = xr.Dataset() - - ds['clock'] = ('clock', [0, 2, 4, 6, 8], - {clock_key: np.uint8(True), mclock_key: np.uint8(True)}) - ds['out'] = ('out', [0, 4, 8], {clock_key: np.uint8(True)}) - - ds['grid__x_size'] = ((), 10, {'description': 'grid size'}) - ds['quantity__quantity'] = ('x', np.zeros(10), - {'description': 'a quantity'}) - ds['some_process__some_param'] = ((), 1, {'description': 'some parameter'}) - ds['other_process__other_param'] = ('clock', [1, 2, 3, 4, 5], - {'description': 'other parameter'}) - - ds['clock'].attrs[svars_key] = 'quantity__quantity' - ds['out'].attrs[svars_key] = ('other_process__other_effect,' - 'some_process__some_effect') - ds.attrs[svars_key] = 'grid__x' - - return ds - - -@pytest.fixture -def ds_model_interface(model, input_dataset): - return DatasetModelInterface(model, input_dataset) +def example_process_obj(): + return ExampleProcess() diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index 4ace2687..b7510d69 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -1,99 +1,153 @@ from textwrap import dedent from io import StringIO +import attr import pytest -from xsimlab.variable.base import Variable -from xsimlab.process import Process +from xsimlab.variable import VarIntent, VarType +from xsimlab.process import (filter_variables, get_process_cls, get_process_obj, + NotAProcessClassError) from xsimlab.tests.conftest import ExampleProcess +# from xsimlab.variable.base import Variable +# from xsimlab.process import Process +# from xsimlab.tests.conftest import ExampleProcess -class TestProcessBase(object): - def test_new(self): - with pytest.raises(TypeError) as excinfo: - class InvalidProcess(ExampleProcess): - var = Variable(()) - assert "subclassing a subclass" in str(excinfo.value) +def test_get_process_cls(example_process_obj): + assert get_process_cls(ExampleProcess) is ExampleProcess + assert get_process_cls(example_process_obj) is ExampleProcess - with pytest.raises(AttributeError) as excinfo: - class InvalidProcess2(Process): - class Meta: - time_dependent = True - invalid_meta_attr = 'invalid' - assert "invalid attribute" in str(excinfo.value) - # test extract variable objects vs. other attributes - assert getattr(ExampleProcess, 'no_var', False) - assert not getattr(ExampleProcess, 'var', False) - assert set(['var', 'var_list', 'var_group', 'diag']) == ( - set(ExampleProcess._variables.keys())) +def test_get_process_obj(example_process_obj): + assert get_process_obj(example_process_obj) is example_process_obj + assert type(get_process_obj(ExampleProcess)) is ExampleProcess - # test Meta attributes - assert ExampleProcess._meta == {'time_dependent': False} +def test_get_process_invalid(): + class NotAProcess(object): + pass -class TestProcess(object): + with pytest.raises(NotAProcessClassError) as excinfo: + get_process_cls(NotAProcess) + get_process_obj(NotAProcess) + assert "is not a process-decorated class" in str(excinfo.value) - def test_constructor(self, process): - # test dict-like vs. attribute access - assert process['var'] is process._variables['var'] - assert process.var is process._variables['var'] - # test deep copy variable objects - ExampleProcess._variables['var'].state = 2 - assert process._variables['var'].state != ( - ExampleProcess._variables['var'].state) +def test_filter_variables(): + func = lambda kw: set(filter_variables(ExampleProcess, **kw).keys()) - # test assign process to diagnostics - assert process['diag']._process_obj is process + expected = {'in_var', 'out_var', 'inout_var', + 'in_foreign_var', 'out_foreign_var', + 'group_var', 'od_var'} + assert func({}) == expected - def test_clone(self, process): - cloned_process = process.clone() - assert process['var'] is not cloned_process['var'] + expected = {'in_var', 'out_var', 'inout_var'} + assert func({'var_type': 'variable'}) == expected - def test_variables(self, process): - assert set(['var', 'var_list', 'var_group', 'diag']) == ( - set(process.variables.keys())) + expected = {'in_var', 'in_foreign_var', 'group_var'} + assert func({'intent': 'in'}) - def test_meta(self, process): - assert process.meta == {'time_dependent': False} + expected = {'out_var', 'out_foreign_var', 'od_var'} + assert func({'intent': 'out'}) - def test_name(self, process): - assert process.name == "ExampleProcess" + expected = {'out_var'} + assert func({'group': 'group1'}) - process._name = "my_process" - assert process.name == "my_process" + expected = {'in_var', 'inout_var', 'in_foreign_var', 'od_var'} + ff = lambda var: ( + var.metadata['var_type'] != VarType.GROUP and + var.metadata['intent'] != VarIntent.OUT + ) + assert func({'func': ff}) - def test_run_step(self, process): - with pytest.raises(NotImplementedError) as excinfo: - process.run_step(1) - assert "no method" in str(excinfo.value) - def test_info(self, process, process_repr): - for cls_or_obj in [ExampleProcess, process]: - buf = StringIO() - cls_or_obj.info(buf=buf) - actual = buf.getvalue() - assert actual == process_repr +# class TestProcessBase(object): - class EmptyProcess(Process): - pass +# def test_new(self): +# with pytest.raises(TypeError) as excinfo: +# class InvalidProcess(ExampleProcess): +# var = Variable(()) +# assert "subclassing a subclass" in str(excinfo.value) - expected = dedent("""\ - Variables: - *empty* - Meta: - time_dependent: True""") +# with pytest.raises(AttributeError) as excinfo: +# class InvalidProcess2(Process): +# class Meta: +# time_dependent = True +# invalid_meta_attr = 'invalid' +# assert "invalid attribute" in str(excinfo.value) - buf = StringIO() - EmptyProcess.info(buf=buf) - actual = buf.getvalue() - assert actual == expected +# # test extract variable objects vs. other attributes +# assert getattr(ExampleProcess, 'no_var', False) +# assert not getattr(ExampleProcess, 'var', False) +# assert set(['var', 'var_list', 'var_group', 'diag']) == ( +# set(ExampleProcess._variables.keys())) - def test_repr(self, process, process_repr): - expected = '\n'.join( - ["", - process_repr] - ) - assert repr(process) == expected +# # test Meta attributes +# assert ExampleProcess._meta == {'time_dependent': False} + + +# class TestProcess(object): + +# def test_constructor(self, process): +# # test dict-like vs. attribute access +# assert process['var'] is process._variables['var'] +# assert process.var is process._variables['var'] + +# # test deep copy variable objects +# ExampleProcess._variables['var'].state = 2 +# assert process._variables['var'].state != ( +# ExampleProcess._variables['var'].state) + +# # test assign process to diagnostics +# assert process['diag']._process_obj is process + +# def test_clone(self, process): +# cloned_process = process.clone() +# assert process['var'] is not cloned_process['var'] + +# def test_variables(self, process): +# assert set(['var', 'var_list', 'var_group', 'diag']) == ( +# set(process.variables.keys())) + +# def test_meta(self, process): +# assert process.meta == {'time_dependent': False} + +# def test_name(self, process): +# assert process.name == "ExampleProcess" + +# process._name = "my_process" +# assert process.name == "my_process" + +# def test_run_step(self, process): +# with pytest.raises(NotImplementedError) as excinfo: +# process.run_step(1) +# assert "no method" in str(excinfo.value) + +# def test_info(self, process, process_repr): +# for cls_or_obj in [ExampleProcess, process]: +# buf = StringIO() +# cls_or_obj.info(buf=buf) +# actual = buf.getvalue() +# assert actual == process_repr + +# class EmptyProcess(Process): +# pass + +# expected = dedent("""\ +# Variables: +# *empty* +# Meta: +# time_dependent: True""") + +# buf = StringIO() +# EmptyProcess.info(buf=buf) +# actual = buf.getvalue() +# assert actual == expected + +# def test_repr(self, process, process_repr): +# expected = '\n'.join( +# ["", +# process_repr] +# ) +# assert repr(process) == expected diff --git a/xsimlab/tests/test_utils.py b/xsimlab/tests/test_utils.py index 89633d93..99d13fb3 100644 --- a/xsimlab/tests/test_utils.py +++ b/xsimlab/tests/test_utils.py @@ -1,6 +1,25 @@ +import attr import pytest from xsimlab import utils +from xsimlab.tests.conftest import ExampleProcess + + +def test_variables_dict(): + assert all([isinstance(var, attr.Attribute) + for var in utils.variables_dict(ExampleProcess).values()]) + + assert 'other_attrib' not in utils.variables_dict(ExampleProcess) + + +def test_has_method(): + assert utils.has_method(ExampleProcess(), 'compute_od_var') + assert not utils.has_method(ExampleProcess(), 'invalid_meth') + + +def test_maybe_to_list(): + assert utils.maybe_to_list([1]) == [1] + assert utils.maybe_to_list(1) == [1] def test_import_required(): diff --git a/xsimlab/tests/test_variable.py b/xsimlab/tests/test_variable.py index 1d85bd42..5b977b4d 100644 --- a/xsimlab/tests/test_variable.py +++ b/xsimlab/tests/test_variable.py @@ -1,247 +1,26 @@ -from collections import OrderedDict - import pytest -import xarray as xr - -from xsimlab.variable.base import (Variable, ForeignVariable, - DiagnosticVariable, VariableList, - ValidationError) -from xsimlab.variable.custom import (NumberVariable, FloatVariable, - IntegerVariable) -from xsimlab.tests.conftest import SomeProcess, OtherProcess, Quantity - - -class TestVariable(object): - - def test_constructor(self): - # verify allowed_dims - for allowed_dims in (tuple(), list(), ''): - var = Variable(allowed_dims) - assert var.allowed_dims == ((),) - - for allowed_dims in ('x', ['x'], ('x')): - var = Variable(allowed_dims) - assert var.allowed_dims == (('x',),) - - var = Variable(('x', 'y')) - assert var.allowed_dims == (('x', 'y'),) - - var = Variable([(), 'x', ('x', 'y')]) - assert var.allowed_dims == ((), ('x',), ('x', 'y')) - - def test_validators(self): - # verify default validators + user supplied validators - validator_func = lambda xr_var: xr_var is not None - - class MyVariable(Variable): - default_validators = [validator_func] - - var = MyVariable((), validators=[validator_func]) - assert var.validators == [validator_func, validator_func] - - def test_validate_dimensions(self): - var = Variable([(), 'x', ('x', 'y')]) - - with pytest.raises(ValidationError) as excinfo: - var.validate_dimensions(('x', 'z')) - assert 'invalid dimensions' in str(excinfo.value) - - var.validate_dimensions(('time', 'x'), ignore_dims=['time']) - - def test_to_xarray_variable(self): - attrs = {'units': 'm'} - description = 'x var' - xr_var_attrs = attrs.copy() - xr_var_attrs.update({'description': description}) - - var = Variable('x', description=description, attrs=attrs) - xr_var = var.to_xarray_variable(('x', [1, 2])) - expected_xr_var = xr.Variable('x', data=[1, 2], attrs=xr_var_attrs) - xr.testing.assert_identical(xr_var, expected_xr_var) - - var = Variable((), default_value=1) - - xr_var = var.to_xarray_variable(2) - expected_xr_var = xr.Variable((), data=2) - xr.testing.assert_identical(xr_var, expected_xr_var) - - # test default value - xr_var = var.to_xarray_variable(None) - expected_xr_var = xr.Variable((), data=1) - xr.testing.assert_identical(xr_var, expected_xr_var) - - # test variable name - xr_var = var.to_xarray_variable([1, 2]) - expected_xr_var = xr.Variable('this_variable', data=[1, 2]) - expected_xr_var = expected_xr_var.to_index_variable() - xr.testing.assert_identical(xr_var, expected_xr_var) - - def test_repr(self): - var = Variable([(), 'x', ('x', 'y')]) - expected_repr = "" - assert repr(var) == expected_repr - - -class TestForeignVariable(object): - - @pytest.fixture - def some_process(self): - """A instance of the process in which the original variable is - declared. - """ - return SomeProcess() - - @pytest.fixture - def foreign_var_cls(self): - """A foreign variable with no Process instance assigned.""" - return ForeignVariable(SomeProcess, 'some_param') - - @pytest.fixture - def foreign_var(self, some_process): - """A foreign variable with an assigned instance of SomeProcess.""" - fvar = ForeignVariable(SomeProcess, 'some_param') - fvar._other_process_obj = some_process - return fvar - - def test_ref_process(self, foreign_var, foreign_var_cls, some_process): - assert foreign_var.ref_process is some_process - assert foreign_var_cls.ref_process is SomeProcess - - def test_ref_var(self, foreign_var, some_process): - assert foreign_var.ref_var is some_process.some_param - - def test_properties(self, foreign_var, some_process): - for prop in ('state', 'value', 'rate', 'change'): - # test foreign getter - setattr(some_process.some_param, prop, 1) - assert getattr(foreign_var, prop) == 1 - - # test foreign setter - setattr(foreign_var, prop, 2) - assert getattr(some_process.some_param, prop) == 2 - - def test_repr(self, foreign_var, foreign_var_cls): - expected_repr = "" - assert repr(foreign_var) == expected_repr - assert repr(foreign_var_cls) == expected_repr - - -class TestDiagnosticVariable(object): - - @pytest.fixture - def quantity(self): - """An instance of the Quantity process that defines some diagnostics.""" - proc = Quantity() - proc.some_derived_quantity.assign_process_obj(proc) - proc.other_derived_quantity.assign_process_obj(proc) - return proc - - def test_decorator(self, quantity): - assert isinstance(quantity.some_derived_quantity, DiagnosticVariable) - assert isinstance(quantity.other_derived_quantity, DiagnosticVariable) - - assert quantity.some_derived_quantity.description == ( - "some derived quantity.") - assert quantity.other_derived_quantity.attrs == {'units': 'm'} - - def test_state(self, quantity): - assert quantity.some_derived_quantity.state == 1 - assert quantity.other_derived_quantity.state == 2 - - def test_call(self, quantity): - assert quantity.some_derived_quantity() == 1 - assert quantity.other_derived_quantity() == 2 - - def test_repr(self, quantity): - expected_repr = "" - assert repr(quantity.some_derived_quantity) == expected_repr - assert repr(quantity.other_derived_quantity) == expected_repr - - -class TestVariableList(object): - - def test_constructor(self): - var_list = VariableList([Variable(()), Variable(('x'))]) - assert isinstance(var_list, tuple) - - with pytest.raises(ValueError) as excinfo: - _ = VariableList([2, Variable(())]) - assert "found variables mixed" in str(excinfo.value) - - -class TestVariableGroup(object): - - def test_iter(self): - some_process = SomeProcess() - other_process = OtherProcess() - quantity = Quantity() - - with pytest.raises(ValueError) as excinfo: - _ = list(quantity.all_effects) - assert "cannot retrieve variables" in str(excinfo.value) - - processes_dict = OrderedDict([('some_process', some_process), - ('other_process', other_process), - ('quantity', quantity)]) - quantity.all_effects._set_variables(processes_dict) - - expected = [some_process.some_effect, other_process.other_effect] - for var, proc in zip(quantity.all_effects, processes_dict.values()): - var._other_process_obj = proc - - fvar_list = [var.ref_var for var in quantity.all_effects] - assert fvar_list == expected - - def test_repr(self): - quantity = Quantity() - - expected_repr = "" - assert repr(quantity.all_effects) == expected_repr - - -class TestNumberVariable(object): - - def test_validate(self): - var = NumberVariable((), bounds=(0, 1)) - for data in (-1, [-1, 0], [-1, 1], [0, 2], 2): - xr_var = var.to_xarray_variable(data) - with pytest.raises(ValidationError) as excinfo: - var.validate(xr_var) - assert "out of bounds" in str(excinfo.value) - - for ib in [(True, False), (False, True), (False, False)]: - var = NumberVariable((), bounds=(0, 1), inclusive_bounds=ib) - xr_var = var.to_xarray_variable([0, 1]) - with pytest.raises(ValidationError) as excinfo: - var.validate(xr_var) - assert "out of bounds" in str(excinfo.value) - - -class TestFloatVariable(object): - - def test_validators(self): - var = FloatVariable(()) - - for val in [1, 1.]: - xr_var = xr.Variable((), val) - var.run_validators(xr_var) - xr_var = xr.Variable((), '1') - with pytest.raises(ValidationError) as excinfo: - var.run_validators(xr_var) - assert "invalid dtype" in str(excinfo.value) +from xsimlab.variable import _as_dim_tuple -class TestIntegerVariable(object): +@pytest.mark.parametrize("dims,expected", [ + ((), ((),)), + ([], ((),)), + ('', ((),)), + (('x'), (('x',),)), + (['x'], (('x',),)), + ('x', (('x',),)), + (('x', 'y'), (('x', 'y'),)), + ([(), 'x', ('x', 'y')], ((), ('x',), ('x', 'y'))) +]) +def test_as_dim_tuple(dims, expected): + assert _as_dim_tuple(dims) == expected - def test_validators(self): - var = IntegerVariable(()) - xr_var = xr.Variable((), 1) - var.run_validators(xr_var) +def test_as_dim_tuple_invalid(): + invalid_dims = ['x', 'y', ('x', 'y'), ('y', 'x')] - for val in [1., '1']: - xr_var = xr.Variable((), val) - with pytest.raises(ValidationError) as excinfo: - var.run_validators(xr_var) - assert "invalid dtype" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + _as_dim_tuple(invalid_dims) + assert "following combinations" in str(excinfo) + assert "('x',), ('y',) and ('x', 'y'), ('y', 'x')" in str(excinfo) From f0213b928a05491ef90551321edcdd526f0aaada Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 20 Apr 2018 14:55:07 +0200 Subject: [PATCH 63/97] add attrs as dependency --- ci/requirements-py34.yml | 1 + ci/requirements-py35.yml | 1 + ci/requirements-py36.yml | 1 + doc/environment.yml | 1 + setup.py | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index 2a501592..fac39a68 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -2,6 +2,7 @@ name: test_env_py34 channels: - conda-forge dependencies: + - attrs - python=3.4 - pytest - numpy diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index 0d930e13..f44638ce 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -2,6 +2,7 @@ name: test_env_py35 channels: - conda-forge dependencies: + - attrs - python=3.5 - pytest - numpy diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index 8197b98a..272a85b3 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -2,6 +2,7 @@ name: test_env_py36 channels: - conda-forge dependencies: + - attrs - python=3.6 - pytest - numpy diff --git a/doc/environment.yml b/doc/environment.yml index 7476a860..95f183ec 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -3,6 +3,7 @@ channels: - conda-forge - defaults dependencies: + - attrs - python=3.5 - numpy=1.12 - pandas=0.22 diff --git a/setup.py b/setup.py index 7e8cbf60..5f7222ee 100755 --- a/setup.py +++ b/setup.py @@ -20,6 +20,6 @@ long_description=(open('README.rst').read() if exists('README.rst') else ''), python_requires='>=3.4', - install_requires=['numpy', 'xarray >= 0.8.0'], + install_requires=['attrs', 'numpy', 'xarray >= 0.8.0'], tests_require=['pytest >= 3.3.0'], zip_safe=False) From b8efce5ec7247224d73266b8b11d9065626bb6d7 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 20 Apr 2018 16:57:49 +0200 Subject: [PATCH 64/97] fix read-only process class properties --- xsimlab/process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 71bf621e..4494f7c7 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -123,7 +123,7 @@ def get_target_variable(var): # TODO: maybe remove this? not even sure such a cycle may happen # unless we allow later providing other values than classes as first # argument of `foreign` - if (target_process_cls, target_var) in visited: + if (target_process_cls, target_var) in visited: # pragma: no cover cycle = '->'.join(['{}.{}'.format(cls.__name__, var.name) if cls is not None else var.name for cls, var in visited]) @@ -234,7 +234,7 @@ def put_in_store(self, value): return property(fget=get_on_demand, doc=var_doc) - elif var_type == VarIntent.IN: + elif var_intent == VarIntent.IN: return property(fget=get_from_store, doc=var_doc) else: From 5f0b41a2de4d2f7af3294c499dbca1543488641a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 20 Apr 2018 17:53:54 +0200 Subject: [PATCH 65/97] (wip) update tests --- xsimlab/tests/conftest.py | 62 ++++++++++++++++++++++++---- xsimlab/tests/test_process.py | 77 ++++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 41 deletions(-) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index 6d6035c6..4db7f1a0 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -10,25 +10,32 @@ @xs.process class SomeProcess(object): - """Just used for foreign variable in ExampleProcess.""" - ref_var = xs.variable() + """Just used for foreign variables in ExampleProcess.""" + some_var = xs.variable(group='some_group') + some_od_var = xs.on_demand(group='some_group') + + @some_od_var.compute + def compute_some_od_var(self): + return 0 @xs.process -class OtherProcess(object): - """Just used for foreign variable in ExampleProcess.""" - ref_var = xs.variable() +class AnotherProcess(object): + """Just used for foreign variables in ExampleProcess.""" + another_var = xs.variable() + some_var = xs.foreign(SomeProcess, 'some_var') @xs.process class ExampleProcess(object): """A process with complete interface for testing.""" in_var = xs.variable() - out_var = xs.variable(group='group1', intent='out') + out_var = xs.variable(group='example_group', intent='out') inout_var = xs.variable(intent='inout') - in_foreign_var = xs.foreign(SomeProcess, 'ref_var') - out_foreign_var = xs.foreign(OtherProcess, 'ref_var', intent='out') - group_var = xs.group('group2') + in_foreign_var = xs.foreign(SomeProcess, 'some_var') + in_foreign_var2 = xs.foreign(AnotherProcess, 'some_var') + out_foreign_var = xs.foreign(AnotherProcess, 'another_var', intent='out') + group_var = xs.group('some_group') od_var = xs.on_demand() other_attrib = attr.attrib(init=False, cmp=False, repr=False) @@ -42,3 +49,40 @@ def compute_od_var(self): @pytest.fixture def example_process_obj(): return ExampleProcess() + + +def _init_process(p_cls, p_name, store, store_keys=None, od_keys=None): + p_obj = p_cls() + p_obj.__xsimlab_name__ = p_name + p_obj.__xsimlab_store__ = store + p_obj.__xsimlab_store_keys__ = store_keys or {} + p_obj.__xsimlab_od_keys__ = od_keys or {} + return p_obj + + +@pytest.fixture +def example_processes_with_store(): + store = {} + + some_process = _init_process( + SomeProcess, 'some_process', store, + store_keys={'some_var': ('some_process', 'some_var')} + ) + another_process = _init_process( + AnotherProcess, 'another_process', store, + store_keys={'another_var': ('another_process', 'another_var'), + 'some_var': ('some_process', 'some_var')} + ) + example_process = _init_process( + ExampleProcess, 'example_process', store, + store_keys={'in_var': ('example_process', 'in_var'), + 'out_var': ('example_process', 'out_var'), + 'inout_var': ('example_process', 'inout_var'), + 'in_foreign_var': ('some_process', 'some_var'), + 'in_foreign_var2': ('some_process', 'some_var'), + 'out_foreign_var': ('another_process', 'another_var'), + 'group_var': [('some_process', 'some_var')]}, + od_keys={'group_var': [('some_process', 'some_od_var')]} + ) + + return some_process, another_process, example_process diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index b7510d69..c03bc2f5 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -5,15 +5,26 @@ import pytest from xsimlab.variable import VarIntent, VarType -from xsimlab.process import (filter_variables, get_process_cls, get_process_obj, - NotAProcessClassError) -from xsimlab.tests.conftest import ExampleProcess +from xsimlab.process import (ensure_process_decorated, filter_variables, + get_process_cls, get_process_obj, + get_target_variable, NotAProcessClassError) +from xsimlab.utils import variables_dict +from xsimlab.tests.conftest import ExampleProcess, SomeProcess # from xsimlab.variable.base import Variable # from xsimlab.process import Process # from xsimlab.tests.conftest import ExampleProcess +def test_ensure_process_decorated(): + class NotAProcess(object): + pass + + with pytest.raises(NotAProcessClassError) as excinfo: + ensure_process_decorated(NotAProcess) + assert "is not a process-decorated class" in str(excinfo.value) + + def test_get_process_cls(example_process_obj): assert get_process_cls(ExampleProcess) is ExampleProcess assert get_process_cls(example_process_obj) is ExampleProcess @@ -24,42 +35,44 @@ def test_get_process_obj(example_process_obj): assert type(get_process_obj(ExampleProcess)) is ExampleProcess -def test_get_process_invalid(): - class NotAProcess(object): - pass - - with pytest.raises(NotAProcessClassError) as excinfo: - get_process_cls(NotAProcess) - get_process_obj(NotAProcess) - assert "is not a process-decorated class" in str(excinfo.value) - +@pytest.mark.parametrize('kwargs,expected', [ + ({}, {'in_var', 'out_var', 'inout_var', 'in_foreign_var', + 'in_foreign_var2', 'out_foreign_var', 'group_var', 'od_var'}), + ({'var_type': 'variable'}, {'in_var', 'out_var', 'inout_var'}), + ({'intent': 'in'}, {'in_var', 'in_foreign_var', 'in_foreign_var2', + 'group_var'}), + ({'intent': 'out'}, {'out_var', 'out_foreign_var', 'od_var'}), + ({'group': 'example_group'}, {'out_var'}), + ({'func': lambda var: ( + var.metadata['var_type'] != VarType.GROUP and + var.metadata['intent'] != VarIntent.OUT)}, + {'in_var', 'inout_var', 'in_foreign_var', 'in_foreign_var2'}) +]) +def test_filter_variables(kwargs, expected): + assert set(filter_variables(ExampleProcess, **kwargs)) == expected -def test_filter_variables(): - func = lambda kw: set(filter_variables(ExampleProcess, **kw).keys()) - expected = {'in_var', 'out_var', 'inout_var', - 'in_foreign_var', 'out_foreign_var', - 'group_var', 'od_var'} - assert func({}) == expected +@pytest.mark.parametrize('var_name,expected_p_cls,expected_var_name', [ + ('in_var', ExampleProcess, 'in_var'), + ('in_foreign_var', SomeProcess, 'some_var'), + ('in_foreign_var2', SomeProcess, 'some_var') # test foreign of foreign +]) +def test_get_target_variable(var_name, expected_p_cls, expected_var_name): + var = variables_dict(ExampleProcess)[var_name] + expected_var = variables_dict(expected_p_cls)[expected_var_name] - expected = {'in_var', 'out_var', 'inout_var'} - assert func({'var_type': 'variable'}) == expected + actual_p_cls, actual_var = get_target_variable(var) - expected = {'in_var', 'in_foreign_var', 'group_var'} - assert func({'intent': 'in'}) + if expected_p_cls is ExampleProcess: + assert actual_p_cls is None + else: + assert actual_p_cls is expected_p_cls - expected = {'out_var', 'out_foreign_var', 'od_var'} - assert func({'intent': 'out'}) + assert actual_var is expected_var - expected = {'out_var'} - assert func({'group': 'group1'}) - expected = {'in_var', 'inout_var', 'in_foreign_var', 'od_var'} - ff = lambda var: ( - var.metadata['var_type'] != VarType.GROUP and - var.metadata['intent'] != VarIntent.OUT - ) - assert func({'func': ff}) +def test_process_properties(example_processes_with_store): + pass # class TestProcessBase(object): From 57608b027778cc70b8e34bf57fd4ed54ed10d2c4 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 11:56:53 +0200 Subject: [PATCH 66/97] fix property for group variables including on-demand targets --- xsimlab/process.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 4494f7c7..72039052 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -223,7 +223,7 @@ def put_in_store(self, value): elif (var_type == VarType.FOREIGN and var_intent == VarIntent.OUT and target_intent == VarIntent.OUT): raise ValueError("Conflict between foreign variable {!r} and its " - "target variable {!r}, both have intent 'out'." + "target variable {!r}, both have intent='out'." .format(var.name, target_str)) elif target_type == VarType.ON_DEMAND: @@ -264,14 +264,17 @@ def _make_property_group(var): var_name = var.name def getter_store_or_on_demand(self): + model = self.__xsimlab_model__ store_keys = self.__xsimlab_store_keys__.get(var_name, []) - od_keys = self.__xsimlab_od_keys.get(var_name, []) + od_keys = self.__xsimlab_od_keys__.get(var_name, []) for key in store_keys: yield self.__xsimlab_store__[key] for key in od_keys: - yield getattr(*key) + p_name, v_name = key + p_obj = model._processes[p_name] + yield getattr(p_obj, v_name) return property(fget=getter_store_or_on_demand, doc=var_details(var)) From be1ba82e72fdd7b7caf220ace5ee51e65d0617b6 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 12:11:26 +0200 Subject: [PATCH 67/97] don't need to check if foreign targeting on-demand have intent='in' --- xsimlab/process.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/xsimlab/process.py b/xsimlab/process.py index 72039052..a0cfdd7c 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -227,11 +227,6 @@ def put_in_store(self, value): .format(var.name, target_str)) elif target_type == VarType.ON_DEMAND: - if var_intent != VarIntent.IN: - raise ValueError("Variable {!r} targeting on-demand variable " - "{!r} should have intent='in' (found {!r})" - .format(var.name, target_str, var_intent.value)) - return property(fget=get_on_demand, doc=var_doc) elif var_intent == VarIntent.IN: From c8fb54547c8c5261988e3f289347e34927d3a326 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 12:12:51 +0200 Subject: [PATCH 68/97] wip update tests --- xsimlab/tests/conftest.py | 34 ++++++++++++----- xsimlab/tests/test_process.py | 68 +++++++++++++++++++++++++++++++--- xsimlab/tests/test_variable.py | 10 ++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index 4db7f1a0..f2b7e5bb 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -11,12 +11,12 @@ @xs.process class SomeProcess(object): """Just used for foreign variables in ExampleProcess.""" - some_var = xs.variable(group='some_group') + some_var = xs.variable(group='some_group', intent='out') some_od_var = xs.on_demand(group='some_group') @some_od_var.compute def compute_some_od_var(self): - return 0 + return 1 @xs.process @@ -32,18 +32,21 @@ class ExampleProcess(object): in_var = xs.variable() out_var = xs.variable(group='example_group', intent='out') inout_var = xs.variable(intent='inout') + od_var = xs.on_demand() + in_foreign_var = xs.foreign(SomeProcess, 'some_var') in_foreign_var2 = xs.foreign(AnotherProcess, 'some_var') out_foreign_var = xs.foreign(AnotherProcess, 'another_var', intent='out') + in_foreign_od_var = xs.foreign(SomeProcess, 'some_od_var') + group_var = xs.group('some_group') - od_var = xs.on_demand() other_attrib = attr.attrib(init=False, cmp=False, repr=False) other_attr = "this is not a xsimlab variable attribute" @od_var.compute def compute_od_var(self): - return 1 + return 0 @pytest.fixture @@ -51,9 +54,10 @@ def example_process_obj(): return ExampleProcess() -def _init_process(p_cls, p_name, store, store_keys=None, od_keys=None): +def _init_process(p_cls, p_name, model, store, store_keys=None, od_keys=None): p_obj = p_cls() p_obj.__xsimlab_name__ = p_name + p_obj.__xsimlab_model__ = model p_obj.__xsimlab_store__ = store p_obj.__xsimlab_store_keys__ = store_keys or {} p_obj.__xsimlab_od_keys__ = od_keys or {} @@ -61,20 +65,25 @@ def _init_process(p_cls, p_name, store, store_keys=None, od_keys=None): @pytest.fixture -def example_processes_with_store(): +def processes_with_store(): + class FakeModel(object): + def __init__(self): + self._processes = {} + + model = FakeModel() store = {} some_process = _init_process( - SomeProcess, 'some_process', store, + SomeProcess, 'some_process', model, store, store_keys={'some_var': ('some_process', 'some_var')} ) another_process = _init_process( - AnotherProcess, 'another_process', store, + AnotherProcess, 'another_process', model, store, store_keys={'another_var': ('another_process', 'another_var'), 'some_var': ('some_process', 'some_var')} ) example_process = _init_process( - ExampleProcess, 'example_process', store, + ExampleProcess, 'example_process', model, store, store_keys={'in_var': ('example_process', 'in_var'), 'out_var': ('example_process', 'out_var'), 'inout_var': ('example_process', 'inout_var'), @@ -82,7 +91,12 @@ def example_processes_with_store(): 'in_foreign_var2': ('some_process', 'some_var'), 'out_foreign_var': ('another_process', 'another_var'), 'group_var': [('some_process', 'some_var')]}, - od_keys={'group_var': [('some_process', 'some_od_var')]} + od_keys={'in_foreign_od_var': ('some_process', 'some_od_var'), + 'group_var': [('some_process', 'some_od_var')]} ) + model._processes.update({'some_process': some_process, + 'another_process': another_process, + 'example_process': example_process}) + return some_process, another_process, example_process diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index c03bc2f5..c00d7a7f 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -4,6 +4,7 @@ import attr import pytest +import xsimlab as xs from xsimlab.variable import VarIntent, VarType from xsimlab.process import (ensure_process_decorated, filter_variables, get_process_cls, get_process_obj, @@ -37,16 +38,18 @@ def test_get_process_obj(example_process_obj): @pytest.mark.parametrize('kwargs,expected', [ ({}, {'in_var', 'out_var', 'inout_var', 'in_foreign_var', - 'in_foreign_var2', 'out_foreign_var', 'group_var', 'od_var'}), + 'in_foreign_var2', 'out_foreign_var', 'in_foreign_od_var', + 'group_var', 'od_var'}), ({'var_type': 'variable'}, {'in_var', 'out_var', 'inout_var'}), ({'intent': 'in'}, {'in_var', 'in_foreign_var', 'in_foreign_var2', - 'group_var'}), + 'in_foreign_od_var', 'group_var'}), ({'intent': 'out'}, {'out_var', 'out_foreign_var', 'od_var'}), ({'group': 'example_group'}, {'out_var'}), ({'func': lambda var: ( var.metadata['var_type'] != VarType.GROUP and var.metadata['intent'] != VarIntent.OUT)}, - {'in_var', 'inout_var', 'in_foreign_var', 'in_foreign_var2'}) + {'in_var', 'inout_var', 'in_foreign_var', 'in_foreign_var2', + 'in_foreign_od_var'}) ]) def test_filter_variables(kwargs, expected): assert set(filter_variables(ExampleProcess, **kwargs)) == expected @@ -71,8 +74,63 @@ def test_get_target_variable(var_name, expected_p_cls, expected_var_name): assert actual_var is expected_var -def test_process_properties(example_processes_with_store): - pass +@pytest.mark.parametrize('p_cls,var_name,prop_is_read_only', [ + (ExampleProcess, 'in_var', True), + (ExampleProcess, 'in_foreign_var', True), + (ExampleProcess, 'group_var', True), + (ExampleProcess, 'od_var', True), + (ExampleProcess, 'inout_var', False), + (ExampleProcess, 'out_var', False), + (ExampleProcess, 'out_foreign_var', False) +]) +def test_process_properties_readonly(p_cls, var_name, prop_is_read_only): + if prop_is_read_only: + assert getattr(p_cls, var_name).fset is None + else: + assert getattr(p_cls, var_name).fset is not None + + +def test_process_properties_errors(): + with pytest.raises(ValueError) as excinfo: + @xs.process + class Process1(object): + invalid_var = xs.foreign(ExampleProcess, 'group_var') + + assert "links to group variable" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @xs.process + class Process2(object): + invalid_var = xs.foreign(ExampleProcess, 'out_var', intent='out') + + assert "both have intent='out'" in str(excinfo.value) + + with pytest.raises(KeyError) as excinfo: + @xs.process + class Process2(object): + invalid_var = xs.on_demand() + + assert "No compute method found" in str(excinfo.value) + + +def test_process_properties_values(processes_with_store): + some_process, another_process, example_process = processes_with_store + + assert example_process.od_var == 0 + assert example_process.in_foreign_od_var == 1 + + example_process.inout_var = 2 + assert example_process.inout_var == 2 + + example_process.out_foreign_var = 3 + assert another_process.another_var == 3 + + some_process.some_var = 4 + assert another_process.some_var == 4 + assert example_process.in_foreign_var == 4 + assert example_process.in_foreign_var2 == 4 + + assert set(example_process.group_var) == {1, 4} # class TestProcessBase(object): diff --git a/xsimlab/tests/test_variable.py b/xsimlab/tests/test_variable.py index 5b977b4d..faf3d2a8 100644 --- a/xsimlab/tests/test_variable.py +++ b/xsimlab/tests/test_variable.py @@ -1,6 +1,7 @@ import pytest -from xsimlab.variable import _as_dim_tuple +from xsimlab.tests.conftest import ExampleProcess +from xsimlab.variable import _as_dim_tuple, foreign @pytest.mark.parametrize("dims,expected", [ @@ -24,3 +25,10 @@ def test_as_dim_tuple_invalid(): _as_dim_tuple(invalid_dims) assert "following combinations" in str(excinfo) assert "('x',), ('y',) and ('x', 'y'), ('y', 'x')" in str(excinfo) + + +def test_foreign(): + with pytest.raises(ValueError) as excinfo: + foreign(ExampleProcess, 'some_var', intent='inout') + + assert "intent='inout' is not supported" in str(excinfo.value) From 036d17733ed0afab47b9ce80b27b8105637f34f3 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 15:14:26 +0200 Subject: [PATCH 69/97] wip update tests --- xsimlab/formatting.py | 14 ++-- xsimlab/tests/conftest.py | 52 +++++++++++++- xsimlab/tests/test_formatting.py | 47 ++++++++++++- xsimlab/tests/test_process.py | 113 ++++++------------------------- xsimlab/tests/test_stores.py | 18 +++++ 5 files changed, 146 insertions(+), 98 deletions(-) create mode 100644 xsimlab/tests/test_stores.py diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index 04afb89a..c1d4a80f 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -43,8 +43,12 @@ def _summarize_var(var, process, col_width): if var_intent == VarIntent.IN: link_symbol = '<---' + var_intent_str = ' [in]' elif var_intent == VarIntent.OUT: link_symbol = '--->' + var_intent_str = ' [out]' + else: + var_intent_str = '[inout]' if var_type == VarType.GROUP: var_info = '{} group {!r}'.format(link_symbol, var.metadata['group']) @@ -69,10 +73,10 @@ def _summarize_var(var, process, col_width): left_col = pretty_print(" {}".format(var.name), col_width) - right_col = maybe_truncate( - "[{}] {}".format(var_intent.value, var_info), - max_line_length - col_width - ) + right_col = var_intent_str + if var_info: + right_col += maybe_truncate(' ' + var_info, + max_line_length - col_width - 7) return left_col + right_col @@ -113,6 +117,8 @@ def repr_process(process): var_section_details = "\n".join( [_summarize_var(var, process, col_width) for var in variables.values()] ) + if not var_section_details: + var_section_details = " *empty*" stages_implemented = [ " {}".format(m) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index f2b7e5bb..3efcd767 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -2,6 +2,8 @@ fixtures that are used across the tests. """ +from textwrap import dedent + import attr import pytest @@ -29,7 +31,7 @@ class AnotherProcess(object): @xs.process class ExampleProcess(object): """A process with complete interface for testing.""" - in_var = xs.variable() + in_var = xs.variable(dims=['x', ('x', 'y')], description='input variable') out_var = xs.variable(group='example_group', intent='out') inout_var = xs.variable(intent='inout') od_var = xs.on_demand() @@ -54,6 +56,36 @@ def example_process_obj(): return ExampleProcess() +@pytest.fixture(scope='session') +def example_process_repr(): + return dedent("""\ + + Variables: + in_var [in] ('x',) or ('x', 'y') input variable + out_var [out] + inout_var [inout] + od_var [out] + in_foreign_var [in] <--- SomeProcess.some_var + in_foreign_var2 [in] <--- AnotherProcess.some_var + out_foreign_var [out] ---> AnotherProcess.another_var + in_foreign_od_var [in] <--- SomeProcess.some_od_var + group_var [in] <--- group 'some_group' + Simulation stages: + *no stage implemented*""") + + +@pytest.fixture(scope='session') +def in_var_details(): + return dedent("""\ + Input variable + + - type : variable + - intent : in + - dims : (('x',), ('x', 'y')) + - group : None + - attrs : {}""") + + def _init_process(p_cls, p_name, model, store, store_keys=None, od_keys=None): p_obj = p_cls() p_obj.__xsimlab_name__ = p_name @@ -100,3 +132,21 @@ def __init__(self): 'example_process': example_process}) return some_process, another_process, example_process + + +@pytest.fixture(scope='session') +def example_process_in_model_repr(): + return dedent("""\ + + Variables: + in_var [in] ('x',) or ('x', 'y') input variable + out_var [out] + inout_var [inout] + od_var [out] + in_foreign_var [in] <--- some_process.some_var + in_foreign_var2 [in] <--- some_process.some_var + out_foreign_var [out] ---> another_process.another_var + in_foreign_od_var [in] <--- some_process.some_od_var + group_var [in] <--- group 'some_group' + Simulation stages: + *no stage implemented*""") diff --git a/xsimlab/tests/test_formatting.py b/xsimlab/tests/test_formatting.py index 059d7e81..31b51d8e 100644 --- a/xsimlab/tests/test_formatting.py +++ b/xsimlab/tests/test_formatting.py @@ -1,4 +1,8 @@ -from xsimlab.formatting import pretty_print, maybe_truncate, wrap_indent +from textwrap import dedent + +import xsimlab as xs +from xsimlab.formatting import (maybe_truncate, pretty_print, + repr_process, var_details, wrap_indent) def test_maybe_truncate(): @@ -18,3 +22,44 @@ def test_wrap_indent(): expected = 'line1\n line2' assert wrap_indent(text, length=1) == expected + + +def test_var_details(example_process_obj): + var = xs.variable(dims='x', description='a variable') + + expected = dedent("""\ + A variable + + - type : variable + - intent : in + - dims : (('x',),) + - group : None + - attrs : {}""") + + assert var_details(var) == expected + + +def test_process_repr(example_process_obj, processes_with_store, + example_process_repr, example_process_in_model_repr): + assert repr_process(example_process_obj) == example_process_repr + + _, _, process_in_model = processes_with_store + assert repr_process(process_in_model) == example_process_in_model_repr + + @xs.process + class Dummy(object): + def initialize(self): + pass + + def run_step(self): + pass + + expected = dedent("""\ + + Variables: + *empty* + Simulation stages: + initialize + run_step""") + + assert repr_process(Dummy()) == expected diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index c00d7a7f..6041b42b 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -1,21 +1,16 @@ -from textwrap import dedent from io import StringIO -import attr import pytest import xsimlab as xs from xsimlab.variable import VarIntent, VarType from xsimlab.process import (ensure_process_decorated, filter_variables, get_process_cls, get_process_obj, - get_target_variable, NotAProcessClassError) + get_target_variable, NotAProcessClassError, + process_info, variable_info) from xsimlab.utils import variables_dict from xsimlab.tests.conftest import ExampleProcess, SomeProcess -# from xsimlab.variable.base import Variable -# from xsimlab.process import Process -# from xsimlab.tests.conftest import ExampleProcess - def test_ensure_process_decorated(): class NotAProcess(object): @@ -107,12 +102,16 @@ class Process2(object): with pytest.raises(KeyError) as excinfo: @xs.process - class Process2(object): - invalid_var = xs.on_demand() + class Process3(object): + var = xs.on_demand() assert "No compute method found" in str(excinfo.value) +def test_process_properties_docstrings(in_var_details): + assert ExampleProcess.in_var.__doc__ == in_var_details + + def test_process_properties_values(processes_with_store): some_process, another_process, example_process = processes_with_store @@ -133,92 +132,22 @@ def test_process_properties_values(processes_with_store): assert set(example_process.group_var) == {1, 4} -# class TestProcessBase(object): - -# def test_new(self): -# with pytest.raises(TypeError) as excinfo: -# class InvalidProcess(ExampleProcess): -# var = Variable(()) -# assert "subclassing a subclass" in str(excinfo.value) - -# with pytest.raises(AttributeError) as excinfo: -# class InvalidProcess2(Process): -# class Meta: -# time_dependent = True -# invalid_meta_attr = 'invalid' -# assert "invalid attribute" in str(excinfo.value) - -# # test extract variable objects vs. other attributes -# assert getattr(ExampleProcess, 'no_var', False) -# assert not getattr(ExampleProcess, 'var', False) -# assert set(['var', 'var_list', 'var_group', 'diag']) == ( -# set(ExampleProcess._variables.keys())) - -# # test Meta attributes -# assert ExampleProcess._meta == {'time_dependent': False} - - -# class TestProcess(object): - -# def test_constructor(self, process): -# # test dict-like vs. attribute access -# assert process['var'] is process._variables['var'] -# assert process.var is process._variables['var'] - -# # test deep copy variable objects -# ExampleProcess._variables['var'].state = 2 -# assert process._variables['var'].state != ( -# ExampleProcess._variables['var'].state) - -# # test assign process to diagnostics -# assert process['diag']._process_obj is process - -# def test_clone(self, process): -# cloned_process = process.clone() -# assert process['var'] is not cloned_process['var'] - -# def test_variables(self, process): -# assert set(['var', 'var_list', 'var_group', 'diag']) == ( -# set(process.variables.keys())) - -# def test_meta(self, process): -# assert process.meta == {'time_dependent': False} - -# def test_name(self, process): -# assert process.name == "ExampleProcess" - -# process._name = "my_process" -# assert process.name == "my_process" +def test_process_decorator(): + with pytest.raises(NotImplementedError): + @xs.process(autodoc=True) + class Dummy(object): + pass -# def test_run_step(self, process): -# with pytest.raises(NotImplementedError) as excinfo: -# process.run_step(1) -# assert "no method" in str(excinfo.value) -# def test_info(self, process, process_repr): -# for cls_or_obj in [ExampleProcess, process]: -# buf = StringIO() -# cls_or_obj.info(buf=buf) -# actual = buf.getvalue() -# assert actual == process_repr +def test_process_info(example_process_obj, example_process_repr): + buf = StringIO() + process_info(example_process_obj, buf=buf) -# class EmptyProcess(Process): -# pass + assert buf.getvalue() == example_process_repr -# expected = dedent("""\ -# Variables: -# *empty* -# Meta: -# time_dependent: True""") -# buf = StringIO() -# EmptyProcess.info(buf=buf) -# actual = buf.getvalue() -# assert actual == expected +def test_variable_info(in_var_details): + buf = StringIO() + variable_info(ExampleProcess, 'in_var', buf=buf) -# def test_repr(self, process, process_repr): -# expected = '\n'.join( -# ["", -# process_repr] -# ) -# assert repr(process) == expected + assert buf.getvalue() == in_var_details diff --git a/xsimlab/tests/test_stores.py b/xsimlab/tests/test_stores.py new file mode 100644 index 00000000..a5c9bf17 --- /dev/null +++ b/xsimlab/tests/test_stores.py @@ -0,0 +1,18 @@ +import numpy as np + +from xsimlab.stores import InMemoryOutputStore + + +def test_in_memory_output_store(): + out_store = InMemoryOutputStore() + key = ('some_process', 'some_var') + + arr = np.array([1, 2, 3]) + out_store.append(key, arr) + arr[:] = [4, 5, 6] + out_store.append(key, arr) + + expected = np.array([[1, 2, 3], + [4, 5, 6]]) + + assert np.all(out_store[key] == expected) From 5c8a743944cd736d8c7d7cdf561a7be954ac6844 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 21:40:06 +0200 Subject: [PATCH 70/97] fix group variable targeting on_demand variables --- xsimlab/model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index 5c87e3f2..fc6a5bc2 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -62,6 +62,9 @@ def _get_var_key(self, p_name, var): if var_type == VarType.VARIABLE: store_key = (p_name, var.name) + elif var_type == VarType.ON_DEMAND: + od_key = (p_name, var.name) + elif var_type == VarType.FOREIGN: target_p_cls, target_var = get_target_variable(var) target_p_name = self._reverse_lookup.get(target_p_cls, None) @@ -73,10 +76,7 @@ def _get_var_key(self, p_name, var): .format(target_p_cls.__name__, var.name, p_name) ) - if target_var.metadata['var_type'] == VarType.ON_DEMAND: - od_key = (target_p_name, target_var.name) - else: - store_key = (target_p_name, target_var.name) + store_key, od_key = self._get_var_key(target_p_name, target_var) elif var_type == VarType.GROUP: var_group = var.metadata['group'] From 1425d4ffc9fa32d1938bae6c344f0299df02ee3a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 21:41:03 +0200 Subject: [PATCH 71/97] fix process dependencies --- xsimlab/model.py | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index fc6a5bc2..dad146dc 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -8,6 +8,18 @@ from .formatting import repr_model +def _flatten_keys(key_seq): + flat_keys = [] + + for k in key_seq: + if not isinstance(k, tuple): + flat_keys += _flatten_keys(k) + else: + flat_keys.append(k) + + return flat_keys + + class _ModelBuilder(object): """Used to iteratively build a new model. @@ -221,19 +233,38 @@ def get_process_dependencies(self): """ self._dep_processes = {k: set() for k in self._processes_obj} + flat_keys = {} + for p_name, p_obj in self._processes_obj.items(): - store_keys = p_obj.__xsimlab_store_keys__ - od_keys = p_obj.__xsimlab_od_keys__ + flat_keys[p_name] = _flatten_keys([ + p_obj.__xsimlab_store_keys__.values(), + p_obj.__xsimlab_od_keys__.values() + ]) - for var_name, key in store_keys.items(): - self._maybe_add_dependency(p_name, p_obj, var_name, key) + for p_name, p_obj in self._processes_obj.items(): + out_vars = filter_variables(p_obj, intent=VarIntent.OUT) - for var_name, key in od_keys.items(): - self._maybe_add_dependency(p_name, p_obj, var_name, key) + for var in out_vars.values(): + if var.metadata['var_type'] == VarType.ON_DEMAND: + key = p_obj.__xsimlab_od_keys__[var.name] + else: + key = p_obj.__xsimlab_store_keys__[var.name] + + for pn in self._processes_obj: + if pn != p_name and key in flat_keys[pn]: + self._dep_processes[pn].add(p_name) + + # store_keys = p_obj.__xsimlab_store_keys__ + # od_keys = p_obj.__xsimlab_od_keys__ + + # for var_name, key in store_keys.items(): + # self._maybe_add_dependency(p_name, p_obj, var_name, key) + + # for var_name, key in od_keys.items(): + # self._maybe_add_dependency(p_name, p_obj, var_name, key) self._dep_processes = {k: list(v) for k, v in self._dep_processes.items()} - return self._dep_processes def _sort_processes(self): From d9dfac20475eefaed15f57b2f87748ebe57d036e Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 23 Apr 2018 21:41:41 +0200 Subject: [PATCH 72/97] wip update tests --- xsimlab/tests/conftest.py | 88 +++++++++ xsimlab/tests/test_model.py | 350 ++++++++++++++++++++--------------- xsimlab/tests/test_stores.py | 3 +- 3 files changed, 293 insertions(+), 148 deletions(-) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index 3efcd767..f441b553 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -6,6 +6,7 @@ import attr import pytest +import numpy as np import xsimlab as xs @@ -150,3 +151,90 @@ def example_process_in_model_repr(): group_var [in] <--- group 'some_group' Simulation stages: *no stage implemented*""") + + +@xs.process +class Profile(object): + u = xs.variable(dims='x', description='quantity u', intent='inout') + u_diffs = xs.group('diff') + u_opp = xs.on_demand(dims='x') + + def initialize(self): + self.u_change = np.zeros_like(self.u) + + def run_step(self, *args): + self.u_change[:] = np.sum((d for d in self.u_diffs)) + + def finalize_step(self): + self.u += self.u_change + + def finalize(self): + self.u[:] = 0. + + @u_opp.compute + def _get_u_opposite(self): + return -self.u + + +@xs.process +class InitProfile(object): + n_points = xs.variable(description='nb. of profile points') + position = xs.variable() + u_init = xs.foreign(Profile, 'u', intent='out') + + def initialize(self): + self.u_init = np.zeros(self.n_points) + self.u_init[self.position] = 1. + + +@xs.process +class Roll(object): + shift = xs.variable(description=('number of places by which elements' + 'of profile u are shifted at each ' + 'time step')) + u = xs.foreign(Profile, 'u') + u_diff = xs.variable(dims='x', group='diff', intent='out') + + def run_step(self, *args): + self.u_diff = np.roll(self.u, self.shift) - self.u + + +@xs.process +class Add(object): + offset = xs.variable(description=('offset * dt added every time step ' + 'to profile u')) + u_diff = xs.variable(dims='x', group='diff', intent='out') + + def run_step(self, dt): + self.u_diff = self.offset * dt + + +@xs.process +class AddOnDemand(object): + offset = xs.variable(description='offset added to profile u') + u_diff = xs.on_demand(group='diff') + + @u_diff.compute + def _compute_u_diff(self): + self.u_diff = self.offset + + +@pytest.fixture +def model(): + return xs.Model({'roll': Roll, + 'add': Add, + 'profile': Profile}) + + +@pytest.fixture +def alternative_model(): + return xs.Model({'roll': Roll, + 'add': AddOnDemand, + 'profile': Profile, + 'init_profile': InitProfile}) + + +@pytest.fixture +def simple_model(): + return xs.Model({'roll': Roll, + 'profile': Profile}) diff --git a/xsimlab/tests/test_model.py b/xsimlab/tests/test_model.py index a1e9270b..ba2baaae 100644 --- a/xsimlab/tests/test_model.py +++ b/xsimlab/tests/test_model.py @@ -2,161 +2,217 @@ import numpy as np from numpy.testing import assert_array_equal -from xsimlab.variable.base import (Variable, ForeignVariable, VariableList, - VariableGroup) -from xsimlab.process import Process from xsimlab.model import Model -from xsimlab.tests.conftest import (Grid, SomeProcess, OtherProcess, Quantity, - PlugProcess) +from xsimlab.tests.conftest import AddOnDemand, InitProfile + + +class TestModelBuilder(object): + + def test_bind_processes(self, model): + assert model._processes['profile'].__xsimlab_model__ is model + assert model._processes['profile'].__xsimlab_name__ == 'profile' + + @pytest.mark.parametrize('p_name,expected_store_keys,expected_od_keys', [ + ('init_profile', + {'n_points': ('init_profile', 'n_points'), + 'position': ('init_profile', 'position'), + 'u_init': ('profile', 'u')}, + {} + ), + ('profile', + {'u': ('profile', 'u'), + 'u_diffs': [('roll', 'u_diff')]}, + {'u_diffs': [('add', 'u_diff')], 'u_opp': ('profile', 'u_opp')} + ), + ('roll', + {'shift': ('roll', 'shift'), 'u': ('profile', 'u'), + 'u_diff': ('roll', 'u_diff')}, + {} + ), + ('add', + {'offset': ('add', 'offset')}, + {'u_diff': ('add', 'u_diff')} + ) + ]) + def test_set_process_keys(self, alternative_model, p_name, + expected_store_keys, expected_od_keys): + p_obj = alternative_model._processes[p_name] + actual_store_keys = p_obj.__xsimlab_store_keys__ + actual_od_keys = p_obj.__xsimlab_od_keys__ + + # key order is not ensured for group variables + if isinstance(expected_store_keys, list): + actual_store_keys = set(actual_store_keys) + expected_store_keys = set(expected_store_keys) + if isinstance(expected_od_keys, list): + actual_od_keys = set(actual_od_keys) + expected_od_keys = set(expected_od_keys) + + assert actual_store_keys == expected_store_keys + assert actual_od_keys == expected_od_keys -@pytest.fixture -def model(model): - """Override fixture defined in conftest.py, return a model - with values set for some of its variables. - """ - model.grid.x_size.value = 10 - model.quantity.quantity.state = np.zeros(10) - model.some_process.some_param.value = 1 +class TestModel(object): - return model + def test_update_processes(self, model, alternative_model): + a_model = model.update_processes({'add': AddOnDemand, + 'init_profile': InitProfile}) + assert a_model == alternative_model + @pytest.mark.parametrize('p_names', ['add', ['add']]) + def test_drop_processes(self, model, simple_model, p_names): + s_model = model.drop_processes(p_names) + assert s_model == simple_model -class TestModel(object): - def test_constructor(self, model): - # test invalid processes - with pytest.raises(TypeError): - Model({'not_a_class': Grid()}) - class OtherClass(object): - pass +# @pytest.fixture +# def model(model): +# """Override fixture defined in conftest.py, return a model +# with values set for some of its variables. +# """ +# model.grid.x_size.value = 10 +# model.quantity.quantity.state = np.zeros(10) +# model.some_process.some_param.value = 1 + +# return model + + +# class TestModel(object): + +# def test_constructor(self, model): +# # test invalid processes +# with pytest.raises(TypeError): +# Model({'not_a_class': Grid()}) + +# class OtherClass(object): +# pass + +# with pytest.raises(TypeError) as excinfo: +# Model({'invalid_class': Process}) +# assert "is not a subclass" in str(excinfo.value) + +# with pytest.raises(TypeError) as excinfo: +# Model({'invalid_class': OtherClass}) +# assert "is not a subclass" in str(excinfo.value) + +# # test process ordering +# expected = ['grid', 'some_process', 'other_process', 'quantity'] +# assert list(model) == expected + +# # test dict-like vs. attribute access +# assert model['grid'] is model.grid + +# # test cyclic process dependencies +# class CyclicProcess(Process): +# some_param = ForeignVariable(SomeProcess, 'some_param', +# provided=True) +# some_effect = ForeignVariable(SomeProcess, 'some_effect') + +# processes = {k: type(v) for k, v in model.items()} +# processes.update({'cyclic': CyclicProcess}) + +# with pytest.raises(ValueError) as excinfo: +# Model(processes) +# assert "cycle detected" in str(excinfo.value) + +# def test_input_vars(self, model): +# expected = {'grid': ['x_size'], +# 'some_process': ['some_param'], +# 'other_process': ['other_param'], +# 'quantity': ['quantity']} +# actual = {k: list(v.keys()) for k, v in model.input_vars.items()} +# assert expected == actual + +# def test_is_input(self, model): +# assert model.is_input(model.grid.x_size) is True +# assert model.is_input(('grid', 'x_size')) is True +# assert model.is_input(model.quantity.all_effects) is False +# assert model.is_input(('other_process', 'copy_param')) is False + +# external_variable = Variable(()) +# assert model.is_input(external_variable) is False + +# var_list = [Variable(()), Variable(()), Variable(())] +# variable_list = VariableList(var_list) +# assert model.is_input(variable_list) is False + +# variable_group = VariableGroup('group') +# variable_group._set_variables({}) +# assert model.is_input(variable_group) is False + +# def test_visualize(self, model): +# pytest.importorskip('graphviz') +# ipydisp = pytest.importorskip('IPython.display') + +# result = model.visualize() +# assert isinstance(result, ipydisp.Image) + +# result = model.visualize(show_inputs=True) +# assert isinstance(result, ipydisp.Image) + +# result = model.visualize(show_variables=True) +# assert isinstance(result, ipydisp.Image) + +# result = model.visualize( +# show_only_variable=('quantity', 'quantity')) +# assert isinstance(result, ipydisp.Image) + +# def test_initialize(self, model): +# model.initialize() +# expected = np.arange(10) +# assert_array_equal(model.grid.x.value, expected) + +# def test_run_step(self, model): +# model.initialize() +# model.run_step(100) + +# expected = model.grid.x.value * 2 +# assert_array_equal(model.quantity.quantity.change, expected) - with pytest.raises(TypeError) as excinfo: - Model({'invalid_class': Process}) - assert "is not a subclass" in str(excinfo.value) +# def test_finalize_step(self, model): +# model.initialize() +# model.run_step(100) +# model.finalize_step() - with pytest.raises(TypeError) as excinfo: - Model({'invalid_class': OtherClass}) - assert "is not a subclass" in str(excinfo.value) - - # test process ordering - expected = ['grid', 'some_process', 'other_process', 'quantity'] - assert list(model) == expected +# expected = model.grid.x.value * 2 +# assert_array_equal(model.quantity.quantity.state, expected) - # test dict-like vs. attribute access - assert model['grid'] is model.grid - - # test cyclic process dependencies - class CyclicProcess(Process): - some_param = ForeignVariable(SomeProcess, 'some_param', - provided=True) - some_effect = ForeignVariable(SomeProcess, 'some_effect') - - processes = {k: type(v) for k, v in model.items()} - processes.update({'cyclic': CyclicProcess}) - - with pytest.raises(ValueError) as excinfo: - Model(processes) - assert "cycle detected" in str(excinfo.value) - - def test_input_vars(self, model): - expected = {'grid': ['x_size'], - 'some_process': ['some_param'], - 'other_process': ['other_param'], - 'quantity': ['quantity']} - actual = {k: list(v.keys()) for k, v in model.input_vars.items()} - assert expected == actual - - def test_is_input(self, model): - assert model.is_input(model.grid.x_size) is True - assert model.is_input(('grid', 'x_size')) is True - assert model.is_input(model.quantity.all_effects) is False - assert model.is_input(('other_process', 'copy_param')) is False - - external_variable = Variable(()) - assert model.is_input(external_variable) is False - - var_list = [Variable(()), Variable(()), Variable(())] - variable_list = VariableList(var_list) - assert model.is_input(variable_list) is False - - variable_group = VariableGroup('group') - variable_group._set_variables({}) - assert model.is_input(variable_group) is False - - def test_visualize(self, model): - pytest.importorskip('graphviz') - ipydisp = pytest.importorskip('IPython.display') - - result = model.visualize() - assert isinstance(result, ipydisp.Image) - - result = model.visualize(show_inputs=True) - assert isinstance(result, ipydisp.Image) - - result = model.visualize(show_variables=True) - assert isinstance(result, ipydisp.Image) - - result = model.visualize( - show_only_variable=('quantity', 'quantity')) - assert isinstance(result, ipydisp.Image) - - def test_initialize(self, model): - model.initialize() - expected = np.arange(10) - assert_array_equal(model.grid.x.value, expected) - - def test_run_step(self, model): - model.initialize() - model.run_step(100) - - expected = model.grid.x.value * 2 - assert_array_equal(model.quantity.quantity.change, expected) - - def test_finalize_step(self, model): - model.initialize() - model.run_step(100) - model.finalize_step() - - expected = model.grid.x.value * 2 - assert_array_equal(model.quantity.quantity.state, expected) - - def test_finalize(self, model): - model.finalize() - assert model.some_process.some_effect.rate == 0 - - def test_clone(self, model): - cloned = model.clone() - - for (ck, cp), (k, p) in zip(cloned.items(), model.items()): - assert ck == k - assert cp is not p - - def test_update_processes(self, model): - expected = Model({'grid': Grid, - 'plug_process': PlugProcess, - 'some_process': SomeProcess, - 'other_process': OtherProcess, - 'quantity': Quantity}) - actual = model.update_processes({'plug_process': PlugProcess}) - assert list(actual) == list(expected) - - def test_drop_processes(self, model): - - expected = Model({'grid': Grid, - 'some_process': SomeProcess, - 'quantity': Quantity}) - actual = model.drop_processes('other_process') - assert list(actual) == list(expected) - - expected = Model({'grid': Grid, - 'quantity': Quantity}) - actual = model.drop_processes(['some_process', 'other_process']) - assert list(actual) == list(expected) - - def test_repr(self, model, model_repr): - assert repr(model) == model_repr - - expected = "" - assert repr(Model({})) == expected +# def test_finalize(self, model): +# model.finalize() +# assert model.some_process.some_effect.rate == 0 + +# def test_clone(self, model): +# cloned = model.clone() + +# for (ck, cp), (k, p) in zip(cloned.items(), model.items()): +# assert ck == k +# assert cp is not p + +# def test_update_processes(self, model): +# expected = Model({'grid': Grid, +# 'plug_process': PlugProcess, +# 'some_process': SomeProcess, +# 'other_process': OtherProcess, +# 'quantity': Quantity}) +# actual = model.update_processes({'plug_process': PlugProcess}) +# assert list(actual) == list(expected) + +# def test_drop_processes(self, model): + +# expected = Model({'grid': Grid, +# 'some_process': SomeProcess, +# 'quantity': Quantity}) +# actual = model.drop_processes('other_process') +# assert list(actual) == list(expected) + +# expected = Model({'grid': Grid, +# 'quantity': Quantity}) +# actual = model.drop_processes(['some_process', 'other_process']) +# assert list(actual) == list(expected) + +# def test_repr(self, model, model_repr): +# assert repr(model) == model_repr + +# expected = "" +# assert repr(Model({})) == expected diff --git a/xsimlab/tests/test_stores.py b/xsimlab/tests/test_stores.py index a5c9bf17..8939d494 100644 --- a/xsimlab/tests/test_stores.py +++ b/xsimlab/tests/test_stores.py @@ -1,4 +1,5 @@ import numpy as np +from numpy.testing import assert_array_equal from xsimlab.stores import InMemoryOutputStore @@ -15,4 +16,4 @@ def test_in_memory_output_store(): expected = np.array([[1, 2, 3], [4, 5, 6]]) - assert np.all(out_store[key] == expected) + assert_array_equal(out_store[key], expected) From 99bd6257c9fa6f8f6141decee7a5cab281001269 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Tue, 24 Apr 2018 01:04:39 +0200 Subject: [PATCH 73/97] clean up --- xsimlab/model.py | 81 +++++++++++++----------------------------------- 1 file changed, 21 insertions(+), 60 deletions(-) diff --git a/xsimlab/model.py b/xsimlab/model.py index dad146dc..3627e866 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -9,13 +9,16 @@ def _flatten_keys(key_seq): + """Given a nested list of keys, i.e., ``('p_name', 'var_name')`` + tuples, returns a flat list of all keys. + """ flat_keys = [] - for k in key_seq: - if not isinstance(k, tuple): - flat_keys += _flatten_keys(k) + for key in key_seq: + if not isinstance(key, tuple): + flat_keys += _flatten_keys(key) else: - flat_keys.append(k) + flat_keys.append(key) return flat_keys @@ -107,11 +110,11 @@ def _get_group_var_keys(self, group): store_keys = [] od_keys = [] - for p_name, p_obj in self._processes_obj.items(): - for var in filter_variables(p_obj, group=group).values(): - if var.metadata['var_type'] == VarType.GROUP: - continue + filter_group = lambda var: (var.metadata.get('group') == group and + var.metadata['var_type'] != VarType.GROUP) + for p_name, p_obj in self._processes_obj.items(): + for var in filter_variables(p_obj, func=filter_group).values(): store_key, od_key = self._get_var_key(p_name, var) if store_key is not None: @@ -190,79 +193,37 @@ def get_input_variables(self): return self._input_vars - def _maybe_add_dependency(self, p_name, p_obj, var_name, key): - """Maybe add a process dependency based on single variable - ``var_name``, defined in process ``p_name``/``p_obj``, with - the corresponding ``key`` (either store or on-demand key). - - Process 1 depends on process 2 if: - - - process 2 has a foreign variable with intent='out' targeting - a variable declared in process 1 ; - - process 1 has a foreign variable with intent!='out' targeting - a variable declared in process 2 that is not a model input (i.e., - process 2 or a 3rd process provides a value for that variable). - - """ - if isinstance(key, list): - # group variable - for k in key: - self._maybe_add_dependency(p_name, p_obj, var_name, k) - - else: - target_p_name, target_var_name = key - var = filter_variables(p_obj)[var_name] - - if target_p_name == p_name: - # not a foreign variable - pass - - elif var.metadata['intent'] == VarIntent.OUT: - # target process depends on current process - self._dep_processes[target_p_name].add(p_name) - - elif (target_p_name, target_var_name) not in self._input_vars: - # current process depends on target process - self._dep_processes[p_name].add(target_p_name) - def get_process_dependencies(self): """Return a dictionary where keys are each process of the model and - values are lists of dependent processes (or empty lists for processes - that have no dependencies). + values are lists of the names of dependent processes (or empty + lists for processes that have no dependencies). + + Process 1 depends on process 2 if the later declares a + variable (resp. a foreign variable) with intent='out' that + itself (resp. its target variable) is needed in process 1. """ self._dep_processes = {k: set() for k in self._processes_obj} - flat_keys = {} + d_keys = {} # all store/on-demand keys for each process for p_name, p_obj in self._processes_obj.items(): - flat_keys[p_name] = _flatten_keys([ + d_keys[p_name] = _flatten_keys([ p_obj.__xsimlab_store_keys__.values(), p_obj.__xsimlab_od_keys__.values() ]) for p_name, p_obj in self._processes_obj.items(): - out_vars = filter_variables(p_obj, intent=VarIntent.OUT) - - for var in out_vars.values(): + for var in filter_variables(p_obj, intent=VarIntent.OUT).values(): if var.metadata['var_type'] == VarType.ON_DEMAND: key = p_obj.__xsimlab_od_keys__[var.name] else: key = p_obj.__xsimlab_store_keys__[var.name] for pn in self._processes_obj: - if pn != p_name and key in flat_keys[pn]: + if pn != p_name and key in d_keys[pn]: self._dep_processes[pn].add(p_name) - # store_keys = p_obj.__xsimlab_store_keys__ - # od_keys = p_obj.__xsimlab_od_keys__ - - # for var_name, key in store_keys.items(): - # self._maybe_add_dependency(p_name, p_obj, var_name, key) - - # for var_name, key in od_keys.items(): - # self._maybe_add_dependency(p_name, p_obj, var_name, key) - self._dep_processes = {k: list(v) for k, v in self._dep_processes.items()} return self._dep_processes From ea8084c603494cb1c3e968b3ef4eca4286cfe443 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Tue, 24 Apr 2018 20:21:19 +0200 Subject: [PATCH 74/97] wip update tests (moved fixtures in separate modules) --- xsimlab/model.py | 12 +- xsimlab/tests/conftest.py | 246 +------------------------------ xsimlab/tests/fixture_model.py | 107 ++++++++++++++ xsimlab/tests/fixture_process.py | 148 +++++++++++++++++++ xsimlab/tests/test_formatting.py | 10 +- xsimlab/tests/test_model.py | 240 ++++++++++++------------------ xsimlab/tests/test_process.py | 2 +- 7 files changed, 372 insertions(+), 393 deletions(-) create mode 100644 xsimlab/tests/fixture_model.py create mode 100644 xsimlab/tests/fixture_process.py diff --git a/xsimlab/model.py b/xsimlab/model.py index 3627e866..a43d198c 100644 --- a/xsimlab/model.py +++ b/xsimlab/model.py @@ -9,8 +9,9 @@ def _flatten_keys(key_seq): - """Given a nested list of keys, i.e., ``('p_name', 'var_name')`` - tuples, returns a flat list of all keys. + """returns a flat list of keys, i.e., ``('foo', 'bar')`` tuples, from + a nested sequence. + """ flat_keys = [] @@ -226,6 +227,7 @@ def get_process_dependencies(self): self._dep_processes = {k: list(v) for k, v in self._dep_processes.items()} + return self._dep_processes def _sort_processes(self): @@ -391,12 +393,12 @@ def all_vars_dict(self): """ if self._all_vars_dict is None: - inputs = defaultdict(list) + all_vars = defaultdict(list) for p_name, var_name in self._all_vars: - inputs[p_name].append(var_name) + all_vars[p_name].append(var_name) - self._all_vars_dict = dict(inputs) + self._all_vars_dict = dict(all_vars) return self._all_vars_dict diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index f441b553..bab9aa5e 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -1,240 +1,6 @@ -"""This module provides a set of process classes and models as pytest -fixtures that are used across the tests. - -""" -from textwrap import dedent - -import attr -import pytest -import numpy as np - -import xsimlab as xs - - -@xs.process -class SomeProcess(object): - """Just used for foreign variables in ExampleProcess.""" - some_var = xs.variable(group='some_group', intent='out') - some_od_var = xs.on_demand(group='some_group') - - @some_od_var.compute - def compute_some_od_var(self): - return 1 - - -@xs.process -class AnotherProcess(object): - """Just used for foreign variables in ExampleProcess.""" - another_var = xs.variable() - some_var = xs.foreign(SomeProcess, 'some_var') - - -@xs.process -class ExampleProcess(object): - """A process with complete interface for testing.""" - in_var = xs.variable(dims=['x', ('x', 'y')], description='input variable') - out_var = xs.variable(group='example_group', intent='out') - inout_var = xs.variable(intent='inout') - od_var = xs.on_demand() - - in_foreign_var = xs.foreign(SomeProcess, 'some_var') - in_foreign_var2 = xs.foreign(AnotherProcess, 'some_var') - out_foreign_var = xs.foreign(AnotherProcess, 'another_var', intent='out') - in_foreign_od_var = xs.foreign(SomeProcess, 'some_od_var') - - group_var = xs.group('some_group') - - other_attrib = attr.attrib(init=False, cmp=False, repr=False) - other_attr = "this is not a xsimlab variable attribute" - - @od_var.compute - def compute_od_var(self): - return 0 - - -@pytest.fixture -def example_process_obj(): - return ExampleProcess() - - -@pytest.fixture(scope='session') -def example_process_repr(): - return dedent("""\ - - Variables: - in_var [in] ('x',) or ('x', 'y') input variable - out_var [out] - inout_var [inout] - od_var [out] - in_foreign_var [in] <--- SomeProcess.some_var - in_foreign_var2 [in] <--- AnotherProcess.some_var - out_foreign_var [out] ---> AnotherProcess.another_var - in_foreign_od_var [in] <--- SomeProcess.some_od_var - group_var [in] <--- group 'some_group' - Simulation stages: - *no stage implemented*""") - - -@pytest.fixture(scope='session') -def in_var_details(): - return dedent("""\ - Input variable - - - type : variable - - intent : in - - dims : (('x',), ('x', 'y')) - - group : None - - attrs : {}""") - - -def _init_process(p_cls, p_name, model, store, store_keys=None, od_keys=None): - p_obj = p_cls() - p_obj.__xsimlab_name__ = p_name - p_obj.__xsimlab_model__ = model - p_obj.__xsimlab_store__ = store - p_obj.__xsimlab_store_keys__ = store_keys or {} - p_obj.__xsimlab_od_keys__ = od_keys or {} - return p_obj - - -@pytest.fixture -def processes_with_store(): - class FakeModel(object): - def __init__(self): - self._processes = {} - - model = FakeModel() - store = {} - - some_process = _init_process( - SomeProcess, 'some_process', model, store, - store_keys={'some_var': ('some_process', 'some_var')} - ) - another_process = _init_process( - AnotherProcess, 'another_process', model, store, - store_keys={'another_var': ('another_process', 'another_var'), - 'some_var': ('some_process', 'some_var')} - ) - example_process = _init_process( - ExampleProcess, 'example_process', model, store, - store_keys={'in_var': ('example_process', 'in_var'), - 'out_var': ('example_process', 'out_var'), - 'inout_var': ('example_process', 'inout_var'), - 'in_foreign_var': ('some_process', 'some_var'), - 'in_foreign_var2': ('some_process', 'some_var'), - 'out_foreign_var': ('another_process', 'another_var'), - 'group_var': [('some_process', 'some_var')]}, - od_keys={'in_foreign_od_var': ('some_process', 'some_od_var'), - 'group_var': [('some_process', 'some_od_var')]} - ) - - model._processes.update({'some_process': some_process, - 'another_process': another_process, - 'example_process': example_process}) - - return some_process, another_process, example_process - - -@pytest.fixture(scope='session') -def example_process_in_model_repr(): - return dedent("""\ - - Variables: - in_var [in] ('x',) or ('x', 'y') input variable - out_var [out] - inout_var [inout] - od_var [out] - in_foreign_var [in] <--- some_process.some_var - in_foreign_var2 [in] <--- some_process.some_var - out_foreign_var [out] ---> another_process.another_var - in_foreign_od_var [in] <--- some_process.some_od_var - group_var [in] <--- group 'some_group' - Simulation stages: - *no stage implemented*""") - - -@xs.process -class Profile(object): - u = xs.variable(dims='x', description='quantity u', intent='inout') - u_diffs = xs.group('diff') - u_opp = xs.on_demand(dims='x') - - def initialize(self): - self.u_change = np.zeros_like(self.u) - - def run_step(self, *args): - self.u_change[:] = np.sum((d for d in self.u_diffs)) - - def finalize_step(self): - self.u += self.u_change - - def finalize(self): - self.u[:] = 0. - - @u_opp.compute - def _get_u_opposite(self): - return -self.u - - -@xs.process -class InitProfile(object): - n_points = xs.variable(description='nb. of profile points') - position = xs.variable() - u_init = xs.foreign(Profile, 'u', intent='out') - - def initialize(self): - self.u_init = np.zeros(self.n_points) - self.u_init[self.position] = 1. - - -@xs.process -class Roll(object): - shift = xs.variable(description=('number of places by which elements' - 'of profile u are shifted at each ' - 'time step')) - u = xs.foreign(Profile, 'u') - u_diff = xs.variable(dims='x', group='diff', intent='out') - - def run_step(self, *args): - self.u_diff = np.roll(self.u, self.shift) - self.u - - -@xs.process -class Add(object): - offset = xs.variable(description=('offset * dt added every time step ' - 'to profile u')) - u_diff = xs.variable(dims='x', group='diff', intent='out') - - def run_step(self, dt): - self.u_diff = self.offset * dt - - -@xs.process -class AddOnDemand(object): - offset = xs.variable(description='offset added to profile u') - u_diff = xs.on_demand(group='diff') - - @u_diff.compute - def _compute_u_diff(self): - self.u_diff = self.offset - - -@pytest.fixture -def model(): - return xs.Model({'roll': Roll, - 'add': Add, - 'profile': Profile}) - - -@pytest.fixture -def alternative_model(): - return xs.Model({'roll': Roll, - 'add': AddOnDemand, - 'profile': Profile, - 'init_profile': InitProfile}) - - -@pytest.fixture -def simple_model(): - return xs.Model({'roll': Roll, - 'profile': Profile}) +from xsimlab.tests.fixture_process import (example_process_obj, + example_process_repr, + in_var_details, processes_with_store, + example_process_in_model_repr) +from xsimlab.tests.fixture_model import (no_init_model, model, model_repr, + simple_model) diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py new file mode 100644 index 00000000..1d30301d --- /dev/null +++ b/xsimlab/tests/fixture_model.py @@ -0,0 +1,107 @@ +from textwrap import dedent + +import numpy as np +import pytest + +import xsimlab as xs + + +__all__ = ['Profile', 'InitProfile', 'Roll', 'Add', 'AddOnDemand', + 'model', 'no_init_model', 'simple_model'] + + +@xs.process +class Profile(object): + u = xs.variable(dims='x', description='quantity u', intent='inout') + u_diffs = xs.group('diff') + u_opp = xs.on_demand(dims='x') + + def initialize(self): + self.u_change = np.zeros_like(self.u) + + def run_step(self, *args): + self.u_change[:] = np.sum((d for d in self.u_diffs)) + + def finalize_step(self): + self.u += self.u_change + + def finalize(self): + self.u[:] = 0. + + @u_opp.compute + def _get_u_opposite(self): + return -self.u + + +@xs.process +class InitProfile(object): + n_points = xs.variable(description='nb. of profile points') + u = xs.foreign(Profile, 'u', intent='out') + + def initialize(self): + self.u_init = np.zeros(self.n_points) + self.u_init[0] = 1. + + +@xs.process +class Roll(object): + shift = xs.variable(description=('shift profile by a nb. of points')) + u = xs.foreign(Profile, 'u') + u_diff = xs.variable(dims='x', group='diff', intent='out') + + def run_step(self, *args): + self.u_diff = np.roll(self.u, self.shift) - self.u + + +@xs.process +class Add(object): + offset = xs.variable(description=('offset * dt added every time step ' + 'to profile u')) + u_diff = xs.variable(dims='x', group='diff', intent='out') + + def run_step(self, dt): + self.u_diff = self.offset * dt + + +@xs.process +class AddOnDemand(object): + offset = xs.variable(description='offset added to profile u') + u_diff = xs.on_demand(group='diff') + + @u_diff.compute + def _compute_u_diff(self): + self.u_diff = self.offset + + +@pytest.fixture +def model(): + return xs.Model({'roll': Roll, + 'add': AddOnDemand, + 'profile': Profile, + 'init_profile': InitProfile}) + + +@pytest.fixture(scope='session') +def model_repr(): + return dedent("""\ + + init_profile + n_points [in] nb. of profile points + roll + shift [in] shift profile by a nb. of points + add + offset [in] offset added to profile u + profile""") + + +@pytest.fixture +def no_init_model(): + return xs.Model({'roll': Roll, + 'add': Add, + 'profile': Profile}) + + +@pytest.fixture +def simple_model(): + return xs.Model({'roll': Roll, + 'profile': Profile}) diff --git a/xsimlab/tests/fixture_process.py b/xsimlab/tests/fixture_process.py new file mode 100644 index 00000000..2e5b5619 --- /dev/null +++ b/xsimlab/tests/fixture_process.py @@ -0,0 +1,148 @@ +from textwrap import dedent + +import attr +import pytest + +import xsimlab as xs + + +@xs.process +class SomeProcess(object): + """Just used for foreign variables in ExampleProcess.""" + some_var = xs.variable(group='some_group', intent='out') + some_od_var = xs.on_demand(group='some_group') + + @some_od_var.compute + def compute_some_od_var(self): + return 1 + + +@xs.process +class AnotherProcess(object): + """Just used for foreign variables in ExampleProcess.""" + another_var = xs.variable() + some_var = xs.foreign(SomeProcess, 'some_var') + + +@xs.process +class ExampleProcess(object): + """A process with complete interface for testing.""" + in_var = xs.variable(dims=['x', ('x', 'y')], description='input variable') + out_var = xs.variable(group='example_group', intent='out') + inout_var = xs.variable(intent='inout') + od_var = xs.on_demand() + + in_foreign_var = xs.foreign(SomeProcess, 'some_var') + in_foreign_var2 = xs.foreign(AnotherProcess, 'some_var') + out_foreign_var = xs.foreign(AnotherProcess, 'another_var', intent='out') + in_foreign_od_var = xs.foreign(SomeProcess, 'some_od_var') + + group_var = xs.group('some_group') + + other_attrib = attr.attrib(init=False, cmp=False, repr=False) + other_attr = "this is not a xsimlab variable attribute" + + @od_var.compute + def compute_od_var(self): + return 0 + + +@pytest.fixture +def example_process_obj(): + return ExampleProcess() + + +@pytest.fixture(scope='session') +def example_process_repr(): + return dedent("""\ + + Variables: + in_var [in] ('x',) or ('x', 'y') input variable + out_var [out] + inout_var [inout] + od_var [out] + in_foreign_var [in] <--- SomeProcess.some_var + in_foreign_var2 [in] <--- AnotherProcess.some_var + out_foreign_var [out] ---> AnotherProcess.another_var + in_foreign_od_var [in] <--- SomeProcess.some_od_var + group_var [in] <--- group 'some_group' + Simulation stages: + *no stage implemented*""") + + +@pytest.fixture(scope='session') +def in_var_details(): + return dedent("""\ + Input variable + + - type : variable + - intent : in + - dims : (('x',), ('x', 'y')) + - group : None + - attrs : {}""") + + +def _init_process(p_cls, p_name, model, store, store_keys=None, od_keys=None): + p_obj = p_cls() + p_obj.__xsimlab_name__ = p_name + p_obj.__xsimlab_model__ = model + p_obj.__xsimlab_store__ = store + p_obj.__xsimlab_store_keys__ = store_keys or {} + p_obj.__xsimlab_od_keys__ = od_keys or {} + return p_obj + + +@pytest.fixture +def processes_with_store(): + class FakeModel(object): + def __init__(self): + self._processes = {} + + model = FakeModel() + store = {} + + some_process = _init_process( + SomeProcess, 'some_process', model, store, + store_keys={'some_var': ('some_process', 'some_var')} + ) + another_process = _init_process( + AnotherProcess, 'another_process', model, store, + store_keys={'another_var': ('another_process', 'another_var'), + 'some_var': ('some_process', 'some_var')} + ) + example_process = _init_process( + ExampleProcess, 'example_process', model, store, + store_keys={'in_var': ('example_process', 'in_var'), + 'out_var': ('example_process', 'out_var'), + 'inout_var': ('example_process', 'inout_var'), + 'in_foreign_var': ('some_process', 'some_var'), + 'in_foreign_var2': ('some_process', 'some_var'), + 'out_foreign_var': ('another_process', 'another_var'), + 'group_var': [('some_process', 'some_var')]}, + od_keys={'in_foreign_od_var': ('some_process', 'some_od_var'), + 'group_var': [('some_process', 'some_od_var')]} + ) + + model._processes.update({'some_process': some_process, + 'another_process': another_process, + 'example_process': example_process}) + + return some_process, another_process, example_process + + +@pytest.fixture(scope='session') +def example_process_in_model_repr(): + return dedent("""\ + + Variables: + in_var [in] ('x',) or ('x', 'y') input variable + out_var [out] + inout_var [inout] + od_var [out] + in_foreign_var [in] <--- some_process.some_var + in_foreign_var2 [in] <--- some_process.some_var + out_foreign_var [out] ---> another_process.another_var + in_foreign_od_var [in] <--- some_process.some_od_var + group_var [in] <--- group 'some_group' + Simulation stages: + *no stage implemented*""") diff --git a/xsimlab/tests/test_formatting.py b/xsimlab/tests/test_formatting.py index 31b51d8e..cd6939a9 100644 --- a/xsimlab/tests/test_formatting.py +++ b/xsimlab/tests/test_formatting.py @@ -2,7 +2,8 @@ import xsimlab as xs from xsimlab.formatting import (maybe_truncate, pretty_print, - repr_process, var_details, wrap_indent) + repr_process, repr_model, + var_details, wrap_indent) def test_maybe_truncate(): @@ -63,3 +64,10 @@ def run_step(self): run_step""") assert repr_process(Dummy()) == expected + + +def test_model_repr(model, model_repr): + assert repr_model(model) == model_repr + + expected = "" + assert repr(xs.Model({})) == expected diff --git a/xsimlab/tests/test_model.py b/xsimlab/tests/test_model.py index ba2baaae..6a820634 100644 --- a/xsimlab/tests/test_model.py +++ b/xsimlab/tests/test_model.py @@ -1,9 +1,7 @@ import pytest -import numpy as np -from numpy.testing import assert_array_equal -from xsimlab.model import Model -from xsimlab.tests.conftest import AddOnDemand, InitProfile +import xsimlab as xs +from xsimlab.tests.fixture_model import AddOnDemand, InitProfile class TestModelBuilder(object): @@ -14,9 +12,7 @@ def test_bind_processes(self, model): @pytest.mark.parametrize('p_name,expected_store_keys,expected_od_keys', [ ('init_profile', - {'n_points': ('init_profile', 'n_points'), - 'position': ('init_profile', 'position'), - 'u_init': ('profile', 'u')}, + {'n_points': ('init_profile', 'n_points'), 'u': ('profile', 'u')}, {} ), ('profile', @@ -34,9 +30,9 @@ def test_bind_processes(self, model): {'u_diff': ('add', 'u_diff')} ) ]) - def test_set_process_keys(self, alternative_model, p_name, + def test_set_process_keys(self, model, p_name, expected_store_keys, expected_od_keys): - p_obj = alternative_model._processes[p_name] + p_obj = model._processes[p_name] actual_store_keys = p_obj.__xsimlab_store_keys__ actual_od_keys = p_obj.__xsimlab_od_keys__ @@ -51,168 +47,120 @@ def test_set_process_keys(self, alternative_model, p_name, assert actual_store_keys == expected_store_keys assert actual_od_keys == expected_od_keys + def test_get_all_variables(self, model): + assert all([len(t) == 2 for t in model.all_vars]) + assert all([p_name in model for p_name, _ in model.all_vars]) + assert ('profile', 'u') in model.all_vars -class TestModel(object): - - def test_update_processes(self, model, alternative_model): - a_model = model.update_processes({'add': AddOnDemand, - 'init_profile': InitProfile}) - assert a_model == alternative_model - - @pytest.mark.parametrize('p_names', ['add', ['add']]) - def test_drop_processes(self, model, simple_model, p_names): - s_model = model.drop_processes(p_names) - assert s_model == simple_model - + def test_get_input_variables(self, model): + expected = {('init_profile', 'n_points'), + ('roll', 'shift'), + ('add', 'offset')} + assert set(model.input_vars) == expected -# @pytest.fixture -# def model(model): -# """Override fixture defined in conftest.py, return a model -# with values set for some of its variables. -# """ -# model.grid.x_size.value = 10 -# model.quantity.quantity.state = np.zeros(10) -# model.some_process.some_param.value = 1 + def test_get_process_dependencies(self, model): + expected = {'init_profile': [], + 'profile': ['init_profile', 'add', 'roll'], + 'roll': ['init_profile'], + 'add': []} -# return model + actual = model.dependent_processes + for p_name in expected: + # order of dependencies is not ensured + assert set(actual[p_name]) == set(expected[p_name]) -# class TestModel(object): - -# def test_constructor(self, model): -# # test invalid processes -# with pytest.raises(TypeError): -# Model({'not_a_class': Grid()}) - -# class OtherClass(object): -# pass + @pytest.mark.parametrize('p_name,dep_p_name', [ + ('profile', 'init_profile'), + ('profile', 'add'), + ('profile', 'roll'), + ('roll', 'init_profile') + ]) + def test_sort_processes(self, model, p_name, dep_p_name): + p_ordered = list(model) + assert p_ordered.index(p_name) > p_ordered.index(dep_p_name) -# with pytest.raises(TypeError) as excinfo: -# Model({'invalid_class': Process}) -# assert "is not a subclass" in str(excinfo.value) + def test_sort_processes_cycle(self, model): + @xs.process + class Foo(object): + in_var = xs.variable() + out_var = xs.variable(intent='out') -# with pytest.raises(TypeError) as excinfo: -# Model({'invalid_class': OtherClass}) -# assert "is not a subclass" in str(excinfo.value) + @xs.process + class Bar(object): + in_foreign = xs.foreign(Foo, 'out_var') + out_foreign = xs.foreign(Foo, 'in_var', intent='out') -# # test process ordering -# expected = ['grid', 'some_process', 'other_process', 'quantity'] -# assert list(model) == expected + with pytest.raises(RuntimeError) as excinfo: + xs.Model({'foo': Foo, 'bar': Bar}) + assert "Cycle detected" in str(excinfo.value) -# # test dict-like vs. attribute access -# assert model['grid'] is model.grid + def test_get_stage_processes(self, model): + expected = [model['roll'], model['profile']] + assert model._p_run_step == expected -# # test cyclic process dependencies -# class CyclicProcess(Process): -# some_param = ForeignVariable(SomeProcess, 'some_param', -# provided=True) -# some_effect = ForeignVariable(SomeProcess, 'some_effect') -# processes = {k: type(v) for k, v in model.items()} -# processes.update({'cyclic': CyclicProcess}) +class TestModel(object): -# with pytest.raises(ValueError) as excinfo: -# Model(processes) -# assert "cycle detected" in str(excinfo.value) + def test_constructor(self): + with pytest.raises(TypeError) as excinfo: + xs.Model({'init_profile': InitProfile()}) + assert "values must be classes" in str(excinfo.value) -# def test_input_vars(self, model): -# expected = {'grid': ['x_size'], -# 'some_process': ['some_param'], -# 'other_process': ['other_param'], -# 'quantity': ['quantity']} -# actual = {k: list(v.keys()) for k, v in model.input_vars.items()} -# assert expected == actual + # test empty model + assert len(xs.Model({})) == 0 -# def test_is_input(self, model): -# assert model.is_input(model.grid.x_size) is True -# assert model.is_input(('grid', 'x_size')) is True -# assert model.is_input(model.quantity.all_effects) is False -# assert model.is_input(('other_process', 'copy_param')) is False + def test_process_dict_vs_attr_access(self, model): + assert model['profile'] is model.profile -# external_variable = Variable(()) -# assert model.is_input(external_variable) is False + def all_vars_dict(self, model): + assert all([p_name in model for p_name in model.all_vars_dict]) + assert all([isinstance(p_vars, list) + for p_vars in model.all_vars_dict]) + assert 'u' in model.all_vars_dict['profile'] -# var_list = [Variable(()), Variable(()), Variable(())] -# variable_list = VariableList(var_list) -# assert model.is_input(variable_list) is False + def input_vars_dict(self, model): + assert all([p_name in model for p_name in model.input_vars_dict]) + assert all([isinstance(p_vars, list) + for p_vars in model.input_vars_dict]) + assert 'u' in model.input_vars_dict['init_profile'] -# variable_group = VariableGroup('group') -# variable_group._set_variables({}) -# assert model.is_input(variable_group) is False + def test_clone(self, model): + cloned = model.clone() -# def test_visualize(self, model): -# pytest.importorskip('graphviz') -# ipydisp = pytest.importorskip('IPython.display') + zprocesses = zip(cloned.items(), model.items()) -# result = model.visualize() -# assert isinstance(result, ipydisp.Image) + for (c_p_name, c_p_obj), (p_name, p_obj) in zprocesses: + assert c_p_name == p_name + assert c_p_obj is not p_obj -# result = model.visualize(show_inputs=True) -# assert isinstance(result, ipydisp.Image) + def test_update_processes(self, no_init_model, model): + m = no_init_model.update_processes({'add': AddOnDemand, + 'init_profile': InitProfile}) + assert m == model -# result = model.visualize(show_variables=True) -# assert isinstance(result, ipydisp.Image) + @pytest.mark.parametrize('p_names', ['add', ['add']]) + def test_drop_processes(self, no_init_model, simple_model, p_names): + m = no_init_model.drop_processes(p_names) + assert m == simple_model -# result = model.visualize( -# show_only_variable=('quantity', 'quantity')) -# assert isinstance(result, ipydisp.Image) + def test_visualize(self, model): + pytest.importorskip('graphviz') + ipydisp = pytest.importorskip('IPython.display') -# def test_initialize(self, model): -# model.initialize() -# expected = np.arange(10) -# assert_array_equal(model.grid.x.value, expected) + result = model.visualize() + assert isinstance(result, ipydisp.Image) -# def test_run_step(self, model): -# model.initialize() -# model.run_step(100) - -# expected = model.grid.x.value * 2 -# assert_array_equal(model.quantity.quantity.change, expected) + result = model.visualize(show_inputs=True) + assert isinstance(result, ipydisp.Image) -# def test_finalize_step(self, model): -# model.initialize() -# model.run_step(100) -# model.finalize_step() + result = model.visualize(show_variables=True) + assert isinstance(result, ipydisp.Image) -# expected = model.grid.x.value * 2 -# assert_array_equal(model.quantity.quantity.state, expected) + result = model.visualize( + show_only_variable=('profile', 'u')) + assert isinstance(result, ipydisp.Image) -# def test_finalize(self, model): -# model.finalize() -# assert model.some_process.some_effect.rate == 0 - -# def test_clone(self, model): -# cloned = model.clone() - -# for (ck, cp), (k, p) in zip(cloned.items(), model.items()): -# assert ck == k -# assert cp is not p - -# def test_update_processes(self, model): -# expected = Model({'grid': Grid, -# 'plug_process': PlugProcess, -# 'some_process': SomeProcess, -# 'other_process': OtherProcess, -# 'quantity': Quantity}) -# actual = model.update_processes({'plug_process': PlugProcess}) -# assert list(actual) == list(expected) - -# def test_drop_processes(self, model): - -# expected = Model({'grid': Grid, -# 'some_process': SomeProcess, -# 'quantity': Quantity}) -# actual = model.drop_processes('other_process') -# assert list(actual) == list(expected) - -# expected = Model({'grid': Grid, -# 'quantity': Quantity}) -# actual = model.drop_processes(['some_process', 'other_process']) -# assert list(actual) == list(expected) - -# def test_repr(self, model, model_repr): -# assert repr(model) == model_repr - -# expected = "" -# assert repr(Model({})) == expected + def test_repr(self, model, model_repr): + assert repr(model) == model_repr diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index 6041b42b..c780695a 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -9,7 +9,7 @@ get_target_variable, NotAProcessClassError, process_info, variable_info) from xsimlab.utils import variables_dict -from xsimlab.tests.conftest import ExampleProcess, SomeProcess +from xsimlab.tests.fixture_process import ExampleProcess, SomeProcess def test_ensure_process_decorated(): From 4542e4c877edb41a2acf43a83c84e153dabbe6fa Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Tue, 24 Apr 2018 21:19:07 +0200 Subject: [PATCH 75/97] update tests for dot module --- xsimlab/tests/test_dot.py | 50 ++++++++++----------------------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/xsimlab/tests/test_dot.py b/xsimlab/tests/test_dot.py index 224a87e7..d8c19f1e 100644 --- a/xsimlab/tests/test_dot.py +++ b/xsimlab/tests/test_dot.py @@ -5,7 +5,8 @@ import pytest pytest.importorskip("graphviz") -from xsimlab.dot import to_graphviz, dot_graph, hash_variable +from xsimlab.dot import to_graphviz, dot_graph, _hash_variable +from xsimlab.utils import variables_dict # need to parse elements of graphivz's Graph object @@ -48,57 +49,32 @@ def test_to_graphviz(model): g = to_graphviz(model) actual_nodes = _get_graph_nodes(g) actual_edges = _get_graph_edges(g) - expected_nodes = ['grid', 'some_process', 'other_process', 'quantity'] - expected_edges = [ - ('grid', 'some_process'), - ('some_process', 'other_process'), - ('grid', 'other_process'), - ('some_process', 'quantity'), - ('other_process', 'quantity') - ] + expected_nodes = list(model) + expected_edges = [(dep_p_name, p_name) + for p_name, p_deps in model.dependent_processes.items() + for dep_p_name in p_deps] assert sorted(actual_nodes) == sorted(expected_nodes) assert set(actual_edges) == set(expected_edges) g = to_graphviz(model, show_inputs=True) actual_nodes = _get_graph_nodes(g) actual_edges = _get_graph_edges(g) - expected_nodes = ['grid', 'some_process', 'other_process', 'quantity', - 'x_size', 'some_param', 'other_param', 'quantity'] - expected_edges = [ - ('grid', 'some_process'), - ('some_process', 'other_process'), - ('grid', 'other_process'), - ('some_process', 'quantity'), - ('other_process', 'quantity'), - (hash_variable(model.grid.x_size), 'grid'), - (hash_variable(model.some_process.some_param), 'some_process'), - (hash_variable(model.other_process.other_param), 'other_process'), - (hash_variable(model.quantity.quantity), 'quantity') + expected_nodes += [var_name for _, var_name in model.input_vars] + expected_edges += [ + (_hash_variable(variables_dict(type(model[p_name]))[var_name]), p_name) + for p_name, var_name in model.input_vars ] assert sorted(actual_nodes) == sorted(expected_nodes) assert set(actual_edges) == set(expected_edges) g = to_graphviz(model, show_variables=True) actual_nodes = _get_graph_nodes(g) - expected_nodes = ['grid', 'some_process', 'other_process', 'quantity', - 'x', 'copy_param', 'quantity', 'some_effect', 'x', - 'copy_param', 'other_effect', 'quantity', 'x', 'x2', - 'all_effects', 'other_derived_quantity', - 'some_derived_quantity', '"\\"', - '"\\"'] + expected_nodes = list(model) + [var_name for _, var_name in model.all_vars] assert sorted(actual_nodes) == sorted(expected_nodes) - g = to_graphviz(model, show_only_variable=('quantity', 'quantity')) - assert _get_graph_nodes(g).count('quantity') == 4 - - g = to_graphviz(model, show_only_variable=model.some_process.quantity) - assert _get_graph_nodes(g).count('quantity') == 4 - - g = to_graphviz(model, show_only_variable=('some_process', 'some_effect')) + g = to_graphviz(model, show_only_variable=('profile', 'u')) actual_nodes = _get_graph_nodes(g) - expected_nodes = ['grid', 'some_process', 'other_process', - 'quantity', 'some_effect', 'all_effects', - '"\\"'] + expected_nodes = list(model) + ['u'] * 3 assert sorted(actual_nodes) == sorted(expected_nodes) From 4d2ec9eca394268faf80d2bb85a2c15163013a10 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Tue, 24 Apr 2018 21:19:48 +0200 Subject: [PATCH 76/97] update what's new --- doc/whats_new.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 7738d3a3..2224011d 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -72,7 +72,9 @@ changes are effective now! - In ``Model.visualize()`` and ``xsimlab.dot.dot_graph()``, ``show_variables=True`` now shows all model variables including - inputs. + inputs. Items of group variables are not shown anymore as nodes. +- ``Model.visualize()`` and ``xsimlab.dot.dot_graph()`` now only + accept tuples for ``show_only_variable``. - For simplicity, ``Dataset.xsimlab.snapshot_vars`` has been renamed to ``output_vars``. The corresponding arguments in ``create_setup`` and From ca8f1da1aa4d91efefe01f2d5d3d010a671fa8e5 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 30 Apr 2018 17:07:32 +0200 Subject: [PATCH 77/97] fix BaseSimulationDriver and test variable --- xsimlab/drivers.py | 2 +- xsimlab/tests/fixture_model.py | 4 ---- xsimlab/tests/test_variable.py | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index 726cef6c..f0f0005b 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -49,7 +49,7 @@ def update_store(self, input_vars): value = input_vars.get(key) if value is not None: - self.store[key] = copy(value) + self.store[key] = copy.copy(value) def update_output_store(self, output_var_keys): """Update the simulation output store (i.e., append new values to the diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py index 1d30301d..8529a3af 100644 --- a/xsimlab/tests/fixture_model.py +++ b/xsimlab/tests/fixture_model.py @@ -6,10 +6,6 @@ import xsimlab as xs -__all__ = ['Profile', 'InitProfile', 'Roll', 'Add', 'AddOnDemand', - 'model', 'no_init_model', 'simple_model'] - - @xs.process class Profile(object): u = xs.variable(dims='x', description='quantity u', intent='inout') diff --git a/xsimlab/tests/test_variable.py b/xsimlab/tests/test_variable.py index faf3d2a8..e1965677 100644 --- a/xsimlab/tests/test_variable.py +++ b/xsimlab/tests/test_variable.py @@ -1,6 +1,6 @@ import pytest -from xsimlab.tests.conftest import ExampleProcess +from xsimlab.tests.fixture_process import ExampleProcess from xsimlab.variable import _as_dim_tuple, foreign From ab5202b91e528ac85e7e5c6071d6feb6974b0979 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Wed, 2 May 2018 16:51:35 +0200 Subject: [PATCH 78/97] wip update tests --- xsimlab/drivers.py | 78 ++++++++++++----- xsimlab/tests/conftest.py | 3 +- xsimlab/tests/fixture_model.py | 65 +++++++++++++- xsimlab/tests/test_utils.py | 2 +- xsimlab/tests/test_xr_interface.py | 132 ----------------------------- 5 files changed, 119 insertions(+), 161 deletions(-) delete mode 100644 xsimlab/tests/test_xr_interface.py diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index f0f0005b..af11354f 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -69,14 +69,18 @@ def run_model(self): raise NotImplementedError() -def _get_dims_from_variable(array, variable): +def _get_dims_from_variable(array, var, clock): """Given an array with numpy compatible interface and a (xarray-simlab) variable, return dimension labels for the array. """ - for dims in variable.dims: - if len(dims) == array.ndim: + ndim = array.ndim + if clock: + ndim -= 1 # ignore clock dimension + + for dims in var.metadata['dims']: + if len(dims) == ndim: return dims return tuple() @@ -94,20 +98,21 @@ class XarraySimulationDriver(BaseSimulationDriver): """ def __init__(self, dataset, model, store, output_store): + self.dataset = dataset self.model = model super(XarraySimulationDriver, self).__init__( model, store, output_store) - self.output_vars = dataset.xsimlab.output_vars - self.output_save_steps = self._get_output_save_steps() - self.master_clock_dim = dataset.xsimlab.master_clock_dim if self.master_clock_dim is None: raise ValueError("Missing master clock dimension / coordinate") self._check_missing_model_inputs() + self.output_vars = dataset.xsimlab.output_vars + self.output_save_steps = self._get_output_save_steps() + def _check_missing_model_inputs(self): """Check if all model inputs have their corresponding variables in the input Dataset. @@ -146,23 +151,42 @@ def _get_output_save_steps(self): return save_steps + def _get_time_steps(self): + """Return a xarray.DataArray of duration between two + consecutive time steps.""" + mclock = self.dataset[self.master_clock_dim] + return mclock.diff(self.master_clock_dim) + + def _split_clock_inputs(self): + """Return two datasets with time-independent and time-dependent + inputs.""" + ds_in = self.dataset.filter( + lambda var: self.master_clock_dim not in var.dims) + ds_in_clock = self.dataset.filter( + lambda var: self.master_clock_dim in var.dims) + + return ds_in, ds_in_clock + def _set_input_vars(self, dataset): for p_name, var_name in self.model.input_vars: xr_var_name = p_name + '__' + var_name xr_var = dataset.get(xr_var_name) + # TODO: convert 0-d arrays to scalars + if xr_var is not None: self.store[(p_name, var_name)] = xr_var.data.copy() def _maybe_save_output_vars(self, istep): - if istep == -1: - var_keys = self.output_vars.get(None, []) - self.update_output_store(var_keys) + # TODO: optimize this for performance + for clock, var_keys in self.output_vars.items(): + save_output = ( + clock is None and istep == -1 or + clock is not None and self.output_save_steps[clock][istep] + ) - else: - for clock, var_keys in self.output_vars.items(): - if clock is not None and self.snapshot_save[clock][istep]: - self.update_output_store(var_keys) + if save_output: + self.update_output_store(var_keys) def _to_xr_variable(self, key, clock): """Convert an output variable to a xarray.Variable object.""" @@ -174,12 +198,13 @@ def _to_xr_variable(self, key, clock): if clock is None: data = data[0] - dims = _get_dims_from_variable(data, var) + dims = _get_dims_from_variable(data, var, clock) if clock is not None: dims = (clock,) + dims attrs = var.metadata['attrs'].copy() - attrs['description'] = var.metadata['description'] + if var.metadata['description']: + attrs['description'] = var.metadata['description'] return xr.Variable(dims, data, attrs=attrs) @@ -187,6 +212,8 @@ def _get_output_dataset(self): """Return a new dataset as a copy of the input dataset updated with output variables. """ + from .xr_accessor import SimlabAccessor + xr_vars = {} for clock, vars in self.output_vars.items(): @@ -194,7 +221,17 @@ def _get_output_dataset(self): var_name = '__'.join(key) xr_vars[var_name] = self._to_xr_variable(key, clock) - return self.dataset.update(xr_vars, inplace=False) + out_ds = self.dataset.update(xr_vars, inplace=False) + + # remove output_vars attributes in output dataset + for clock in self.output_vars: + if clock is None: + attrs = out_ds.attrs + else: + attrs = out_ds[clock].attrs + attrs.pop(SimlabAccessor._output_vars_key) + + return out_ds def run_model(self): """Run the model and return a new Dataset with all the simulation @@ -206,15 +243,10 @@ def run_model(self): 'finalize_step' stages or at the end of the simulation. """ - ds_in = self.dataset.filter( - lambda var: self.master_clock_dim not in var.dims) - ds_in_clock = self.dataset.filter( - lambda var: self.master_clock_dim in var.dims) - + ds_in, ds_in_clock = self._split_clock_inputs() has_clock_inputs = bool(ds_in_clock.data_vars) - mclock = self.dataset[self.master_clock_dim] - da_dt = mclock.diff(self.master_clock_dim) + da_dt = self._get_time_steps() self._set_input_vars(ds_in) self.model.initialize() diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index bab9aa5e..b38f131b 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -2,5 +2,6 @@ example_process_repr, in_var_details, processes_with_store, example_process_in_model_repr) -from xsimlab.tests.fixture_model import (no_init_model, model, model_repr, +from xsimlab.tests.fixture_model import (in_dataset, out_dataset, + no_init_model, model, model_repr, simple_model) diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py index 8529a3af..831a433e 100644 --- a/xsimlab/tests/fixture_model.py +++ b/xsimlab/tests/fixture_model.py @@ -1,9 +1,11 @@ from textwrap import dedent import numpy as np +import xarray as xr import pytest import xsimlab as xs +from xsimlab.xr_accessor import SimlabAccessor @xs.process @@ -35,8 +37,8 @@ class InitProfile(object): u = xs.foreign(Profile, 'u', intent='out') def initialize(self): - self.u_init = np.zeros(self.n_points) - self.u_init[0] = 1. + self.u = np.zeros(self.n_points) + self.u[0] = 1. @xs.process @@ -53,7 +55,7 @@ def run_step(self, *args): class Add(object): offset = xs.variable(description=('offset * dt added every time step ' 'to profile u')) - u_diff = xs.variable(dims='x', group='diff', intent='out') + u_diff = xs.variable(group='diff', intent='out') def run_step(self, dt): self.u_diff = self.offset * dt @@ -66,7 +68,7 @@ class AddOnDemand(object): @u_diff.compute def _compute_u_diff(self): - self.u_diff = self.offset + return self.offset @pytest.fixture @@ -101,3 +103,58 @@ def no_init_model(): def simple_model(): return xs.Model({'roll': Roll, 'profile': Profile}) + + +@pytest.fixture +def in_dataset(): + clock_key = SimlabAccessor._clock_key + mclock_key = SimlabAccessor._master_clock_key + svars_key = SimlabAccessor._output_vars_key + + ds = xr.Dataset() + + ds['clock'] = ('clock', [0, 2, 4, 6, 8], + {clock_key: np.uint8(True), mclock_key: np.uint8(True)}) + ds['out'] = ('out', [0, 4, 8], {clock_key: np.uint8(True)}) + + ds['init_profile__n_points'] = ( + (), 5, {'description': 'nb. of profile points'}) + ds['roll__shift'] = ( + (), 1, {'description': 'shift profile by a nb. of points'}) + ds['add__offset'] = ( + 'clock', [1, 2, 3, 4, 5], {'description': 'offset added to profile u'}) + + ds['clock'].attrs[svars_key] = 'profile__u' + ds['out'].attrs[svars_key] = ('roll__u_diff,' + 'add__u_diff') + ds.attrs[svars_key] = 'profile__u_opp' + + return ds + + +@pytest.fixture +def out_dataset(in_dataset): + out_ds = in_dataset + + del out_ds.attrs[SimlabAccessor._output_vars_key] + del out_ds.clock.attrs[SimlabAccessor._output_vars_key] + del out_ds.out.attrs[SimlabAccessor._output_vars_key] + out_ds['profile__u_opp'] = ('x', [-10. , -10., -10., -10., -11.]) + out_ds['profile_u'] = ( + ('clock', 'x'), + np.array([[1., 0., 0., 0., 0.], + [1., 2., 1., 1., 1.], + [3., 3., 4., 3., 3.], + [6., 6., 6., 7., 6.], + [10., 10., 10., 10., 11.]]), + {'description': 'quantity u'} + ) + out_ds['roll_u_diff'] = ( + ('out', 'x'), + np.array([[-1., 1., 0., 0., 0.], + [ 0., 0., -1., 1., 0.], + [ 0., 0., 0., -1., 1.]]) + ) + out_ds['add__u_diff'] = ('out', [1, 3, 4]) + + return out_ds diff --git a/xsimlab/tests/test_utils.py b/xsimlab/tests/test_utils.py index 99d13fb3..ce3f651e 100644 --- a/xsimlab/tests/test_utils.py +++ b/xsimlab/tests/test_utils.py @@ -2,7 +2,7 @@ import pytest from xsimlab import utils -from xsimlab.tests.conftest import ExampleProcess +from xsimlab.tests.fixture_process import ExampleProcess def test_variables_dict(): diff --git a/xsimlab/tests/test_xr_interface.py b/xsimlab/tests/test_xr_interface.py deleted file mode 100644 index 73f977ad..00000000 --- a/xsimlab/tests/test_xr_interface.py +++ /dev/null @@ -1,132 +0,0 @@ -import pytest - -import numpy as np -import xarray as xr - -from xsimlab.xr_accessor import SimlabAccessor -from xsimlab.xr_interface import DatasetModelInterface - - -class TestDatasetModelInterface(object): - - def test_constructor(self, model, input_dataset): - ds = xr.Dataset() - with pytest.raises(ValueError) as excinfo: - DatasetModelInterface(model, ds) - assert "missing master clock dimension" in str(excinfo.value) - - invalid_ds = input_dataset.drop('quantity__quantity') - with pytest.raises(KeyError) as excinfo: - DatasetModelInterface(model, invalid_ds) - assert "missing data variables" in str(excinfo.value) - - def test_set_model_inputs(self, input_dataset, ds_model_interface): - ds = input_dataset.drop('other_process__other_param') - ds_model_interface.set_model_inputs(ds) - model = ds_model_interface.model - - assert model.grid.x_size.value == 10 - np.testing.assert_array_equal(model.quantity.quantity.value, - np.zeros(10)) - assert model.some_process.some_param.value == 1 - assert model.other_process.other_param.value is None - - def test_split_data_vars_clock(self, ds_model_interface): - ds_clock, ds_no_clock = ds_model_interface.split_data_vars_clock() - assert 'other_process__other_param' in ds_clock - assert 'other_process__other_param' not in ds_no_clock - - def test_time_step_lengths(self, ds_model_interface): - np.testing.assert_array_equal(ds_model_interface.time_step_lengths, - [2, 2, 2, 2]) - - def test_init_snapshots(self, ds_model_interface): - ds_model_interface.init_snapshots() - - expected = {('quantity', 'quantity'), ('some_process', 'some_effect'), - ('other_process', 'other_effect'), ('grid', 'x')} - assert set(ds_model_interface.snapshot_values) == expected - - expected = {'clock': np.array([True, True, True, True, True]), - 'out': np.array([True, False, True, False, True])} - assert ds_model_interface.snapshot_save.keys() == expected.keys() - for k in expected: - np.testing.assert_array_equal(ds_model_interface.snapshot_save[k], - expected[k]) - - def test_take_snapshot_var(self, ds_model_interface): - ds_model_interface.init_snapshots() - ds_model_interface.set_model_inputs(ds_model_interface.dataset) - - key = ('quantity', 'quantity') - ds_model_interface.take_snapshot_var(key) - expected = np.zeros(10) - actual = ds_model_interface.snapshot_values[key][0] - np.testing.assert_array_equal(actual, expected) - - # ensure snapshot array is a copy - actual[0] = 1 - assert actual[0] != ds_model_interface.model.quantity.quantity.value[0] - - @pytest.mark.parametrize( - 'istep,expected_len', - [(0, [1, 1, 1, 0]), (1, [1, 0, 0, 0]), (-1, [1, 1, 1, 1])] - ) - def test_take_snapshots(self, ds_model_interface, istep, expected_len): - ds_model_interface.init_snapshots() - ds_model_interface.set_model_inputs(ds_model_interface.dataset) - - keys = [('quantity', 'quantity'), ('some_process', 'some_effect'), - ('other_process', 'other_effect'), ('grid', 'x')] - - ds_model_interface.take_snapshots(istep) - for k, length in zip(keys, expected_len): - assert len(ds_model_interface.snapshot_values[k]) == length - - def test_snapshot_to_xarray_variable(self, ds_model_interface): - ds_model_interface.init_snapshots() - ds_model_interface.set_model_inputs(ds_model_interface.dataset) - ds_model_interface.model.initialize() - - ds_model_interface.take_snapshots(0) - - expected = xr.Variable('x', np.zeros(10), - {'description': 'a quantity'}) - actual = ds_model_interface.snapshot_to_xarray_variable( - ('quantity', 'quantity'), clock='clock') - xr.testing.assert_identical(actual, expected) - - ds_model_interface.take_snapshots(-1) - - expected = xr.Variable(('clock', 'x'), np.zeros((2, 10))) - actual = ds_model_interface.snapshot_to_xarray_variable( - ('quantity', 'quantity'), clock='clock') - xr.testing.assert_equal(actual, expected) - - expected = xr.Variable('x', np.arange(10)) - actual = ds_model_interface.snapshot_to_xarray_variable(('grid', 'x')) - xr.testing.assert_equal(actual, expected) - - def test_run_model(self, input_dataset, ds_model_interface): - out_ds = ds_model_interface.run_model() - - expected = input_dataset.copy() - del expected.attrs[SimlabAccessor._snapshot_vars_key] - del expected.clock.attrs[SimlabAccessor._snapshot_vars_key] - del expected.out.attrs[SimlabAccessor._snapshot_vars_key] - expected['grid__x'] = ('x', np.arange(10), {'description': ''}) - expected['quantity__quantity'] = ( - ('clock', 'x'), - np.arange(0, 10, 2)[:, None] * np.arange(10) * 1., - {'description': 'a quantity'} - ) - expected['some_process__some_effect'] = ( - ('out', 'x'), np.tile(np.arange(2, 12), 3).reshape(3, 10), - {'description': ''} - ) - expected['other_process__other_effect'] = ( - ('out', 'x'), np.tile(np.arange(-2, 8), 3).reshape(3, 10), - {'description': ''} - ) - - xr.testing.assert_identical(out_ds, expected) From 0c8ea353d217e6bf8900e2ecbe61803158d7bb8a Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 14:04:05 +0200 Subject: [PATCH 79/97] update tests --- xsimlab/tests/fixture_model.py | 13 +- xsimlab/tests/test_drivers.py | 100 ++++++++++++ xsimlab/tests/test_xr_accessor.py | 249 +++++++++++++++--------------- xsimlab/xr_accessor.py | 93 +++++------ 4 files changed, 274 insertions(+), 181 deletions(-) create mode 100644 xsimlab/tests/test_drivers.py diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py index 831a433e..082416c9 100644 --- a/xsimlab/tests/fixture_model.py +++ b/xsimlab/tests/fixture_model.py @@ -43,7 +43,8 @@ def initialize(self): @xs.process class Roll(object): - shift = xs.variable(description=('shift profile by a nb. of points')) + shift = xs.variable(description=('shift profile by a nb. of points'), + attrs={'units': 'unitless'}) u = xs.foreign(Profile, 'u') u_diff = xs.variable(dims='x', group='diff', intent='out') @@ -120,7 +121,9 @@ def in_dataset(): ds['init_profile__n_points'] = ( (), 5, {'description': 'nb. of profile points'}) ds['roll__shift'] = ( - (), 1, {'description': 'shift profile by a nb. of points'}) + (), 1, + {'description': 'shift profile by a nb. of points', + 'units': 'unitless'}) ds['add__offset'] = ( 'clock', [1, 2, 3, 4, 5], {'description': 'offset added to profile u'}) @@ -139,7 +142,7 @@ def out_dataset(in_dataset): del out_ds.attrs[SimlabAccessor._output_vars_key] del out_ds.clock.attrs[SimlabAccessor._output_vars_key] del out_ds.out.attrs[SimlabAccessor._output_vars_key] - out_ds['profile__u_opp'] = ('x', [-10. , -10., -10., -10., -11.]) + out_ds['profile__u_opp'] = ('x', [-10., -10., -10., -10., -11.]) out_ds['profile_u'] = ( ('clock', 'x'), np.array([[1., 0., 0., 0., 0.], @@ -152,8 +155,8 @@ def out_dataset(in_dataset): out_ds['roll_u_diff'] = ( ('out', 'x'), np.array([[-1., 1., 0., 0., 0.], - [ 0., 0., -1., 1., 0.], - [ 0., 0., 0., -1., 1.]]) + [0., 0., -1., 1., 0.], + [0., 0., 0., -1., 1.]]) ) out_ds['add__u_diff'] = ('out', [1, 3, 4]) diff --git a/xsimlab/tests/test_drivers.py b/xsimlab/tests/test_drivers.py new file mode 100644 index 00000000..880e9f9d --- /dev/null +++ b/xsimlab/tests/test_drivers.py @@ -0,0 +1,100 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from xarray.testing import assert_identical + +from xsimlab.drivers import BaseSimulationDriver, XarraySimulationDriver +from xsimlab.stores import InMemoryOutputStore +from xsimlab.xr_accessor import SimlabAccessor + + +@pytest.fixture +def base_driver(model): + store = {} + out_store = InMemoryOutputStore() + return BaseSimulationDriver(model, store, out_store) + + +@pytest.fixture +def xarray_driver(in_dataset, model): + store = {} + out_store = InMemoryOutputStore() + return XarraySimulationDriver(in_dataset, model, store, out_store) + + +class TestBaseDriver(object): + + def test_bind_store(self, base_driver): + base_driver.store[('init_profile', 'n_points')] = 10 + assert base_driver.model.init_profile.n_points == 10 + + def test_update_store(self, base_driver): + n = [10, 100, 1000] + input_vars = {('init_profile', 'n_points'): n} + base_driver.update_store(input_vars) + + assert base_driver.store[('init_profile', 'n_points')] == n + assert base_driver.store[('init_profile', 'n_points')] is not n + + def test_update_output_store(self, base_driver): + base_driver.store[('init_profile', 'n_points')] = 5 + base_driver.model.init_profile.initialize() + + base_driver.update_output_store([('profile', 'u')]) + + expected = [np.array([1., 0., 0., 0., 0.])] + assert_array_equal( + base_driver.output_store[('profile', 'u')], + expected) + + def test_run_model(self, base_driver): + with pytest.raises(NotImplementedError): + base_driver.run_model() + + +class TestXarraySimulationDriver(object): + + def test_constructor(self, in_dataset, model): + store = {} + out_store = InMemoryOutputStore() + + invalid_ds = in_dataset.drop('clock') + with pytest.raises(ValueError) as excinfo: + XarraySimulationDriver(invalid_ds, model, store, out_store) + assert "Missing master clock" in str(excinfo.value) + + invalid_ds = in_dataset.drop('init_profile__n_points') + with pytest.raises(KeyError) as excinfo: + XarraySimulationDriver(invalid_ds, model, store, out_store) + assert "Missing variables" in str(excinfo.value) + + def test_output_save_steps(self, xarray_driver): + expected = {'clock': np.array([True, True, True, True, True]), + 'out': np.array([True, False, True, False, True])} + + assert xarray_driver.output_save_steps.keys() == expected.keys() + for k in expected: + assert_array_equal(xarray_driver.output_save_steps[k], expected[k]) + + def test_time_step_lengths(self, xarray_driver): + assert_array_equal(xarray_driver._get_time_steps(), [2, 2, 2, 2]) + + def test_split_data_vars_clock(self, xarray_driver): + ds_in, ds_in_clock = xarray_driver._split_clock_inputs() + + assert 'add__offset' in ds_in_clock and 'add__offset' not in ds_in + assert 'roll__shift' in ds_in and 'roll__shift' not in ds_in_clock + + def test_set_input_vars(self, in_dataset, xarray_driver): + in_ds = in_dataset.drop('add__offset') + xarray_driver._set_input_vars(in_ds) + + assert (xarray_driver.store[('init_profile', 'n_points')] == + in_dataset['init_profile__n_points'].data) + assert (xarray_driver.store[('init_profile', 'n_points')] is not + in_dataset['init_profile__n_points'].data) + + def test_run_model(self, in_dataset, out_dataset, xarray_driver): + out_ds_actual = xarray_driver.run_model() + + assert_identical(out_ds_actual, out_dataset) diff --git a/xsimlab/tests/test_xr_accessor.py b/xsimlab/tests/test_xr_accessor.py index 49ecd0dc..b8e06c15 100644 --- a/xsimlab/tests/test_xr_accessor.py +++ b/xsimlab/tests/test_xr_accessor.py @@ -3,7 +3,9 @@ import numpy as np from xsimlab import xr_accessor, create_setup -from xsimlab.xr_accessor import _maybe_get_model_from_context +from xsimlab.xr_accessor import (as_variable_key, + _flatten_inputs, _flatten_outputs, + _maybe_get_model_from_context) def test_filter_accessor(): @@ -14,11 +16,66 @@ def test_filter_accessor(): assert 'x' in filtered.coords and 'y' not in filtered.coords +def test_get_model_from_context(model): + with pytest.raises(TypeError) as excinfo: + _maybe_get_model_from_context(None) + assert "No model found in context" in str(excinfo.value) + + with model as m: + assert _maybe_get_model_from_context(None) is m + + with pytest.raises(TypeError) as excinfo: + _maybe_get_model_from_context('not a model') + assert "is not an instance of xsimlab.Model" in str(excinfo.value) + + +def test_as_variable_key(): + assert as_variable_key(('foo', 'bar')) == ('foo', 'bar') + assert as_variable_key('foo__bar') == ('foo', 'bar') + assert as_variable_key('foo_bar__baz') == ('foo_bar', 'baz') + + with pytest.raises(ValueError) as excinfo: + as_variable_key('foo__bar__baz') + assert "not a valid input variable" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + as_variable_key('foo__') + assert "not a valid input variable" in str(excinfo.value) + + +@pytest.mark.parametrize('input_vars,expected', [ + ({('foo', 'bar'): 1}, {('foo', 'bar'): 1}), + ({'foo__bar': 1}, {('foo', 'bar'): 1}), + ({'foo': {'bar': 1}}, {('foo', 'bar'): 1}) +]) +def test_flatten_inputs(input_vars, expected): + assert _flatten_inputs(input_vars) == expected + + +@pytest.mark.parametrize('output_vars,expected', [ + ({'clock': 'foo__bar'}, {'clock': [('foo', 'bar')]}), + ({'clock': ('foo', 'bar')}, {'clock': [('foo', 'bar')]}), + ({'clock': [('foo', 'bar')]}, {'clock': [('foo', 'bar')]}), + ({'clock': [('foo__bar')]}, {'clock': [('foo', 'bar')]}), + ({'clock': {'foo': 'bar'}}, {'clock': [('foo', 'bar')]}), + ({'clock': {'foo': ['bar', 'baz']}}, + {'clock': [('foo', 'bar'), ('foo', 'baz')]}) +]) +def test_flatten_outputs(output_vars, expected): + assert _flatten_outputs(output_vars) == expected + + +def test_flatten_outputs_error(): + with pytest.raises(ValueError) as excinfo: + _flatten_outputs({'clock': 2}) + assert "Cannot interpret" in str(excinfo.value) + + class TestSimlabAccessor(object): _clock_key = xr_accessor.SimlabAccessor._clock_key _master_clock_key = xr_accessor.SimlabAccessor._master_clock_key - _snapshot_vars_key = xr_accessor.SimlabAccessor._snapshot_vars_key + _output_vars_key = xr_accessor.SimlabAccessor._output_vars_key def test_clock_coords(self): ds = xr.Dataset( @@ -125,42 +182,25 @@ def test_set_snapshot_clock(self): ds.xsimlab._set_snapshot_clock('snap_clock', data=[0, 3, 8], auto_adjust=False) - def test_set_input_vars(self, model): - ds = xr.Dataset() + def test_set_input_vars(self, model, in_dataset): + in_vars = {('init_profile', 'n_points'): 5, + ('roll', 'shift'): 1, + ('add', 'offset'): ('clock', [1, 2, 3, 4, 5])} - with pytest.raises(KeyError) as excinfo: - ds.xsimlab._set_input_vars(model, 'invalid_process', var=1) - assert "no process named" in str(excinfo.value) + ds = xr.Dataset(coords={'clock': [0, 2, 4, 6, 8]}) + ds.xsimlab._set_input_vars(model, in_vars) - with pytest.raises(ValueError) as excinfo: - ds.xsimlab._set_input_vars(model, 'some_process', some_param=0, - invalid_var=1) - assert "not valid input variables" in str(excinfo.value) - - ds.xsimlab._set_input_vars(model, 'quantity', - quantity=('x', np.zeros(10))) - expected = xr.DataArray(data=np.zeros(10), dims='x') - assert "quantity__quantity" in ds - xr.testing.assert_equal(ds['quantity__quantity'], expected) - - # test time and parameter dimensions - ds.xsimlab._set_input_vars(model, model.some_process, some_param=[1, 2]) - expected = xr.DataArray(data=[1, 2], dims='some_process__some_param', - coords={'some_process__some_param': [1, 2]}) - xr.testing.assert_equal(ds['some_process__some_param'], expected) - del ds['some_process__some_param'] - - ds['clock'] = ('clock', [0, 1], {self._master_clock_key: 1}) - ds.xsimlab._set_input_vars(model, 'some_process', - some_param=('clock', [1, 2])) - expected = xr.DataArray(data=[1, 2], dims='clock', - coords={'clock': [0, 1]}) - xr.testing.assert_equal(ds['some_process__some_param'], expected) - - # test optional - ds.xsimlab._set_input_vars(model, 'grid') - expected = xr.DataArray(data=5) - xr.testing.assert_equal(ds['grid__x_size'], expected) + for vname in ('init_profile__n_points', 'roll__shift', 'add__offset'): + # xr.testing.assert_identical also checks attrs of coordinates + # (not needed here) + xr.testing.assert_equal(ds[vname], in_dataset[vname]) + assert ds[vname].attrs == in_dataset[vname].attrs + + in_vars[('not_an', 'input_var')] = None + + with pytest.raises(KeyError) as excinfo: + ds.xsimlab._set_input_vars(model, in_vars) + assert "not valid key(s)" in str(excinfo.value) def test_update_clocks(self, model): ds = xr.Dataset() @@ -193,7 +233,7 @@ def test_update_clocks(self, model): ) assert ds.xsimlab.master_clock_dim == 'clock' - ds.clock.attrs[self._snapshot_vars_key] = 'quantity__quantity' + ds.clock.attrs[self._output_vars_key] = 'profile__u' ds = ds.xsimlab.update_clocks( model=model, @@ -204,91 +244,71 @@ def test_update_clocks(self, model): ) assert 'units' in ds.clock.attrs assert 'calendar' in ds.clock.attrs - assert ds.clock.attrs[self._snapshot_vars_key] == 'quantity__quantity' + assert ds.clock.attrs[self._output_vars_key] == 'profile__u' - def test_update_vars(self, model, input_dataset): - ds = input_dataset.xsimlab.update_vars( + def test_update_vars(self, model, in_dataset): + ds = in_dataset.xsimlab.update_vars( model=model, - input_vars={'some_process': {'some_param': 2}}, - snapshot_vars={'out': {'other_process': 'other_effect'}} + input_vars={('roll', 'shift'): 2}, + output_vars={'out': ('profile', 'u')} ) - var = 'some_process__some_param' - assert not ds[var].equals(input_dataset[var]) - assert not ds['out'].identical(input_dataset['out']) - def test_filter_vars(self, model, input_dataset): - alt_model = model.drop_processes(['other_process']) + assert not ds['roll__shift'].equals(in_dataset['roll__shift']) + assert not ds['out'].identical(in_dataset['out']) + + def test_filter_vars(self, simple_model, in_dataset): + ds = in_dataset.xsimlab.filter_vars(model=simple_model) - ds = input_dataset.xsimlab.filter_vars(model=alt_model) - assert 'other_process__other_param' not in ds + assert 'add__offset' not in ds assert sorted(ds.xsimlab.clock_coords) == ['clock', 'out'] - expected = 'some_process__some_effect' - assert ds.out.attrs[self._snapshot_vars_key] == expected + assert ds.out.attrs[self._output_vars_key] == 'roll__u_diff' - def test_set_snapshot_vars(self, model): + def test_set_output_vars(self, model): ds = xr.Dataset() ds['clock'] = ('clock', [0, 2, 4, 6, 8], {self._clock_key: 1, self._master_clock_key: 1}) - ds['snap_clock'] = ('snap_clock', [0, 4, 8], {self._clock_key: 1}) + ds['out'] = ('out', [0, 4, 8], {self._clock_key: 1}) ds['not_a_clock'] = ('not_a_clock', [0, 1]) with pytest.raises(KeyError) as excinfo: - ds.xsimlab._set_snapshot_vars(model, None, invalid_process='var') - assert "no process named" in str(excinfo.value) + ds.xsimlab._set_output_vars(model, None, [('invalid', 'var')]) + assert "not valid key(s)" in str(excinfo.value) - with pytest.raises(KeyError) as excinfo: - ds.xsimlab._set_snapshot_vars(model, None, quantity='invalid_var') - assert "has no variable" in str(excinfo.value) - - ds.xsimlab._set_snapshot_vars(model, None, grid='x') - assert ds.attrs[self._snapshot_vars_key] == 'grid__x' - - ds.xsimlab._set_snapshot_vars(model, 'clock', - some_process='some_effect', - quantity='quantity') - expected = {'some_process__some_effect', 'quantity__quantity'} - actual = set(ds['clock'].attrs[self._snapshot_vars_key].split(',')) - assert actual == expected + ds.xsimlab._set_output_vars(model, None, [('profile', 'u_opp')]) + assert ds.attrs[self._output_vars_key] == 'profile__u_opp' - ds.xsimlab._set_snapshot_vars(model, 'snap_clock', - other_process=('other_effect', 'x2')) - expected = {'other_process__other_effect', 'other_process__x2'} - actual = set(ds['snap_clock'].attrs[self._snapshot_vars_key].split(',')) - assert actual == expected + ds.xsimlab._set_output_vars(model, 'out', + [('roll', 'u_diff'), ('add', 'u_diff')]) + expected = 'roll__u_diff,add__u_diff' + assert ds['out'].attrs[self._output_vars_key] == expected with pytest.raises(ValueError) as excinfo: - ds.xsimlab._set_snapshot_vars(model, 'not_a_clock', - quantity='quantity') + ds.xsimlab._set_output_vars(model, 'not_a_clock', + [('profile', 'u')]) assert "not a valid clock" in str(excinfo.value) - def test_snapshot_vars(self, model): + def test_output_vars(self, model): ds = xr.Dataset() ds['clock'] = ('clock', [0, 2, 4, 6, 8], {self._clock_key: 1, self._master_clock_key: 1}) - ds['snap_clock'] = ('snap_clock', [0, 4, 8], {self._clock_key: 1}) - # snapshot clock with no snapshot variable (attribute) set - ds['snap_clock2'] = ('snap_clock2', [0, 8], {self._clock_key: 1}) - - ds.xsimlab._set_snapshot_vars(model, None, grid='x') - ds.xsimlab._set_snapshot_vars(model, 'clock', quantity='quantity') - ds.xsimlab._set_snapshot_vars(model, 'snap_clock', - other_process=('other_effect', 'x2')) - - expected = {None: set([('grid', 'x')]), - 'clock': set([('quantity', 'quantity')]), - 'snap_clock': set([('other_process', 'other_effect'), - ('other_process', 'x2')])} - actual = {k: set(v) for k, v in ds.xsimlab.snapshot_vars.items()} - assert actual == expected - - def test_run(self, model, input_dataset): - # safe mode True: model cloned -> values not set in original model - _ = input_dataset.xsimlab.run(model=model) - assert model.quantity.quantity.value is None + ds['out'] = ('out', [0, 4, 8], {self._clock_key: 1}) + # snapshot clock with no output variable (attribute) set + ds['out2'] = ('out2', [0, 8], {self._clock_key: 1}) + ds.xsimlab._set_output_vars(model, None, [('profile', 'u_opp')]) + ds.xsimlab._set_output_vars(model, 'clock', [('profile', 'u')]) + ds.xsimlab._set_output_vars(model, 'out', + [('roll', 'u_diff'), ('add', 'u_diff')]) + + expected = {None: [('profile', 'u_opp')], + 'clock': [('profile', 'u')], + 'out': [('roll', 'u_diff'), ('add', 'u_diff')]} + assert ds.xsimlab.output_vars == expected + + def test_run(self, model, in_dataset): # safe mode False: model not cloned -> values set in original model - _ = input_dataset.xsimlab.run(model=model, safe_mode=False) - assert model.quantity.quantity.value is not None + _ = in_dataset.xsimlab.run(model=model, safe_mode=False) + assert model.profile.u is not None def test_run_multi(self): ds = xr.Dataset() @@ -297,7 +317,7 @@ def test_run_multi(self): ds.xsimlab.run_multi() -def test_create_setup(model, input_dataset): +def test_create_setup(model, in_dataset): expected = xr.Dataset() actual = create_setup(model=model) xr.testing.assert_identical(actual, expected) @@ -305,33 +325,18 @@ def test_create_setup(model, input_dataset): ds = create_setup( model=model, input_vars={ - 'grid': {'x_size': 10}, - 'quantity': {'quantity': ('x', np.zeros(10))}, - 'some_process': {'some_param': 1}, - 'other_process': {'other_param': ('clock', [1, 2, 3, 4, 5])} + 'init_profile': {'n_points': 5}, + ('roll', 'shift'): 1, + 'add__offset': ('clock', [1, 2, 3, 4, 5]) }, clocks={ 'clock': {'data': [0, 2, 4, 6, 8]}, 'out': {'data': [0, 4, 8]}, }, master_clock='clock', - snapshot_vars={ - 'clock': {'quantity': 'quantity'}, - 'out': {'some_process': 'some_effect', - 'other_process': 'other_effect'}, - None: {'grid': 'x'} + output_vars={ + 'clock': 'profile__u', + 'out': [('roll', 'u_diff'), ('add', 'u_diff')], + None: {'profile': 'u_opp'} }) - xr.testing.assert_identical(ds, input_dataset) - - -def test_get_model_from_context(model): - with pytest.raises(TypeError) as excinfo: - _maybe_get_model_from_context(None) - assert "no model found in context" in str(excinfo.value) - - with model as m: - assert _maybe_get_model_from_context(None) is m - - with pytest.raises(TypeError) as excinfo: - _maybe_get_model_from_context('not a model') - assert "is not an instance of xsimlab.Model" in str(excinfo.value) + xr.testing.assert_identical(ds, in_dataset) diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index 66fd37aa..e199f6a4 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -106,7 +106,7 @@ def _flatten_outputs(output_vars): else: var_list += [(p_name, vname) for vname in var_names] - elif isinstance(out_vars, [tuple, str]): + elif isinstance(out_vars, (tuple, str)): var_list = [as_variable_key(out_vars)] elif isinstance(out_vars, list): @@ -123,7 +123,7 @@ def _flatten_outputs(output_vars): @register_dataset_accessor('xsimlab') class SimlabAccessor(object): - """simlab extension to :class:`xarray.Dataset`.""" + """Simlab extension to :class:`xarray.Dataset`.""" _clock_key = '__xsimlab_output_clock__' _master_clock_key = '__xsimlab_master_clock__' @@ -250,14 +250,7 @@ def _set_input_vars(self, model, input_vars): if invalid_inputs: raise KeyError( "{} is/are not valid key(s) for input variables in model {}" - .format(', '.join([k for k in invalid_inputs]), model) - ) - - missing_inputs = set(model.input_vars) - set(input_vars) - if missing_inputs: - raise KeyError( - "Missing value for input variable(s) {}" - .format(', '.join([k for k in missing_inputs])) + .format(', '.join([str(k) for k in invalid_inputs]), model) ) for (p_name, var_name), data in input_vars.items(): @@ -267,9 +260,9 @@ def _set_input_vars(self, model, input_vars): xr_var_name = p_name + '__' + var_name xr_var = as_variable(data) - xr_var.attrs.update(var.metadata['attrs']) if var.metadata['description']: xr_var.attrs['description'] = var.metadata['description'] + xr_var.attrs.update(var.metadata['attrs']) self._ds[xr_var_name] = xr_var @@ -278,54 +271,54 @@ def _set_output_vars(self, model, clock, output_vars): if invalid_outputs: raise KeyError( "{} is/are not valid key(s) for variables in model {}" - .format(', '.join([k for k in invalid_outputs]), model) + .format(', '.join([str(k) for k in invalid_outputs]), model) ) - output_vars = ','.join([p_name + '__' + var_name - for (p_name, var_name) in output_vars]) + output_vars_str = ','.join([p_name + '__' + var_name + for (p_name, var_name) in output_vars]) if clock is None: - self._ds.attrs[self._output_vars_key] = output_vars + self._ds.attrs[self._output_vars_key] = output_vars_str else: if clock not in self.clock_coords: raise ValueError("{!r} coordinate is not a valid clock " "coordinate.".format(clock)) coord = self.clock_coords[clock] - coord.attrs[self._output_vars_key] = output_vars + coord.attrs[self._output_vars_key] = output_vars_str - def _get_output_vars(self, clock, ds_or_coord): - out_attr = ds_or_coord.attrs.get(self._output_vars_key, '') + def _maybe_update_output_vars(self, clock, ds_or_coord, output_vars): + out_attr = ds_or_coord.attrs.get(self._output_vars_key) - if out_attr: - return {clock: [as_variable_key(k) for k in out_attr.split(',')]} - else: - return {} + if out_attr is not None: + output_vars[clock] = [as_variable_key(k) + for k in out_attr.split(',')] @property def output_vars(self): - """Returns a dictionary of snapshot clock dimension names as keys and - output variable names - i.e. lists of (process name, variable name) + """Returns a dictionary of clock dimension names (or None) as keys and + output variable names - i.e. lists of ``('p_name', 'var_name')`` tuples - as values. + """ output_vars = {} for clock, clock_coord in self.clock_coords.items(): - output_vars.update(self._get_output_vars(clock, clock_coord)) + self._maybe_update_output_vars(clock, clock_coord, output_vars) - output_vars.update(self._get_output_vars(None, self._ds)) + self._maybe_update_output_vars(None, self._ds, output_vars) return output_vars def update_clocks(self, model=None, clocks=None, master_clock=None): """Update clock coordinates. - Drop all clock coordinates (if any) and add a new set of master and - snapshot clock coordinates. - Also copy all snapshot-specific attributes of the replaced coordinates. + Add clock coordinates (after dropped all existing clock + coordinates). Output variable attributes are propagate to + the replaced coordinates. - More details about the values allowed for the parameters below can be - found in the doc of :meth:`xsimlab.create_setup`. + More details about the values allowed for the parameters below + can be found in the doc of :meth:`xsimlab.create_setup`. Parameters ---------- @@ -334,8 +327,8 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): clocks : dict of dicts, optional Used to create one or several clock coordinates. master_clock : str or dict, optional - Name (and units/calendar) of the clock coordinate (dimension) to - use as master clock. + Name (and units/calendar) of the clock coordinate + (dimension) to use as master clock. Returns ------- @@ -379,13 +372,9 @@ def update_clocks(self, model=None, clocks=None, master_clock=None): for dim, kwargs in clocks.items(): ds.xsimlab._set_snapshot_clock(dim, **kwargs) - for dim, var_list in self.output_vars.items(): - var_dict = defaultdict(list) - for p_name, var_name in var_list: - var_dict[p_name].append(var_name) - - if dim is None or dim in ds: - ds.xsimlab._set_output_vars(model, dim, **var_dict) + for clock, var_keys in self.output_vars.items(): + if clock is None or clock in ds: + ds.xsimlab._set_output_vars(model, clock, var_keys) return ds @@ -425,9 +414,8 @@ def update_vars(self, model=None, input_vars=None, output_vars=None): ds.xsimlab._set_input_vars(model, _flatten_inputs(input_vars)) if output_vars is not None: - for clock, out_vars in output_vars.items(): - ds.xsimlab._set_output_vars(model, clock, - _flatten_outputs(out_vars)) + for clock, out_vars in _flatten_outputs(output_vars).items(): + ds.xsimlab._set_output_vars(model, clock, out_vars) return ds @@ -462,7 +450,7 @@ def filter_vars(self, model=None): # drop variables drop_variables = [] - for xr_var_name in self._ds: + for xr_var_name in self._ds.variables: if xr_var_name in self.clock_coords: continue @@ -484,19 +472,15 @@ def filter_vars(self, model=None): return ds def _clean_output_dataset(self, ds): - """Return a new dataset after having removed unnecessary attributes.""" - clean_ds = ds.copy() - - for clock in clean_ds.output_vars: + """Remove unnecessary attributes in output dataset ``ds``.""" + for clock in ds.xsimlab.output_vars: if clock is None: - attrs = clean_ds.attrs + attrs = ds.attrs else: - attrs = clean_ds[clock].attrs + attrs = ds[clock].attrs attrs.pop(self._output_vars_key) - return clean_ds - def run(self, model=None, safe_mode=True): """Run the model. @@ -523,9 +507,10 @@ def run(self, model=None, safe_mode=True): store = {} output_store = InMemoryOutputStore() - driver = XarraySimulationDriver(model, self._ds, store, output_store) + driver = XarraySimulationDriver(self._ds, model, store, output_store) - out_ds = driver.run_model().pipe(self._clean_output_dataset) + out_ds = driver.run_model() + self._clean_output_dataset(out_ds) return out_ds From cf93f49001a7b2ae7e0046169ed6f5d58ec9cca7 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 15:17:22 +0200 Subject: [PATCH 80/97] fix tests and increase coverage --- xsimlab/drivers.py | 2 +- xsimlab/tests/test_drivers.py | 15 +++++++++++++-- xsimlab/tests/test_formatting.py | 14 +++++--------- xsimlab/tests/test_model.py | 14 +++++++++----- xsimlab/tests/test_process.py | 10 ++++++++-- xsimlab/tests/test_xr_accessor.py | 17 ++++++++++++----- xsimlab/xr_accessor.py | 18 +++--------------- 7 files changed, 51 insertions(+), 39 deletions(-) diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index af11354f..69dd8e81 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -76,7 +76,7 @@ def _get_dims_from_variable(array, var, clock): """ ndim = array.ndim - if clock: + if clock is not None: ndim -= 1 # ignore clock dimension for dims in var.metadata['dims']: diff --git a/xsimlab/tests/test_drivers.py b/xsimlab/tests/test_drivers.py index 880e9f9d..e0960982 100644 --- a/xsimlab/tests/test_drivers.py +++ b/xsimlab/tests/test_drivers.py @@ -3,9 +3,10 @@ from numpy.testing import assert_array_equal from xarray.testing import assert_identical -from xsimlab.drivers import BaseSimulationDriver, XarraySimulationDriver +import xsimlab as xs +from xsimlab.drivers import (_get_dims_from_variable, BaseSimulationDriver, + XarraySimulationDriver) from xsimlab.stores import InMemoryOutputStore -from xsimlab.xr_accessor import SimlabAccessor @pytest.fixture @@ -52,6 +53,16 @@ def test_run_model(self, base_driver): base_driver.run_model() +@pytest.mark.parametrize('array,clock,expected' , [ + (np.zeros((2, 2)), None, ('x', 'y')), + (np.zeros((2, 2)), 'clock', ('x',)), + (np.array(0), None, tuple()) +]) +def test_get_dims_from_variable(array, clock, expected): + var = xs.variable(dims=[(), ('x',), ('x', 'y')]) + assert _get_dims_from_variable(array, var, clock) == expected + + class TestXarraySimulationDriver(object): def test_constructor(self, in_dataset, model): diff --git a/xsimlab/tests/test_formatting.py b/xsimlab/tests/test_formatting.py index cd6939a9..f0b5bf8b 100644 --- a/xsimlab/tests/test_formatting.py +++ b/xsimlab/tests/test_formatting.py @@ -28,16 +28,12 @@ def test_wrap_indent(): def test_var_details(example_process_obj): var = xs.variable(dims='x', description='a variable') - expected = dedent("""\ - A variable - - - type : variable - - intent : in - - dims : (('x',),) - - group : None - - attrs : {}""") + var_details_str = var_details(var) - assert var_details(var) == expected + assert var_details_str.strip().startswith('A variable') + assert "- type : variable" in var_details_str + assert "- intent : in" in var_details_str + assert "- dims : (('x',),)" in var_details_str def test_process_repr(example_process_obj, processes_with_store, diff --git a/xsimlab/tests/test_model.py b/xsimlab/tests/test_model.py index 6a820634..305b828e 100644 --- a/xsimlab/tests/test_model.py +++ b/xsimlab/tests/test_model.py @@ -108,23 +108,27 @@ def test_constructor(self): xs.Model({'init_profile': InitProfile()}) assert "values must be classes" in str(excinfo.value) + with pytest.raises(KeyError) as excinfo: + xs.Model({'init_profile': InitProfile}) + assert "Process class 'Profile' missing" in str(excinfo.value) + # test empty model assert len(xs.Model({})) == 0 def test_process_dict_vs_attr_access(self, model): assert model['profile'] is model.profile - def all_vars_dict(self, model): + def test_all_vars_dict(self, model): assert all([p_name in model for p_name in model.all_vars_dict]) assert all([isinstance(p_vars, list) - for p_vars in model.all_vars_dict]) + for p_vars in model.all_vars_dict.values()]) assert 'u' in model.all_vars_dict['profile'] - def input_vars_dict(self, model): + def test_input_vars_dict(self, model): assert all([p_name in model for p_name in model.input_vars_dict]) assert all([isinstance(p_vars, list) - for p_vars in model.input_vars_dict]) - assert 'u' in model.input_vars_dict['init_profile'] + for p_vars in model.input_vars_dict.values()]) + assert 'n_points' in model.input_vars_dict['init_profile'] def test_clone(self, model): cloned = model.clone() diff --git a/xsimlab/tests/test_process.py b/xsimlab/tests/test_process.py index c780695a..2978b9f9 100644 --- a/xsimlab/tests/test_process.py +++ b/xsimlab/tests/test_process.py @@ -109,7 +109,10 @@ class Process3(object): def test_process_properties_docstrings(in_var_details): - assert ExampleProcess.in_var.__doc__ == in_var_details + # order of lines in string is not ensured (printed from a dictionary) + to_lines = lambda details_str: sorted(details_str.split('\n')) + + assert to_lines(ExampleProcess.in_var.__doc__) == to_lines(in_var_details) def test_process_properties_values(processes_with_store): @@ -150,4 +153,7 @@ def test_variable_info(in_var_details): buf = StringIO() variable_info(ExampleProcess, 'in_var', buf=buf) - assert buf.getvalue() == in_var_details + # order of lines in string is not ensured (printed from a dictionary) + to_lines = lambda details_str: sorted(details_str.split('\n')) + + assert to_lines(buf.getvalue()) == to_lines(in_var_details) diff --git a/xsimlab/tests/test_xr_accessor.py b/xsimlab/tests/test_xr_accessor.py index b8e06c15..2e259059 100644 --- a/xsimlab/tests/test_xr_accessor.py +++ b/xsimlab/tests/test_xr_accessor.py @@ -257,11 +257,14 @@ def test_update_vars(self, model, in_dataset): assert not ds['out'].identical(in_dataset['out']) def test_filter_vars(self, simple_model, in_dataset): - ds = in_dataset.xsimlab.filter_vars(model=simple_model) + in_dataset['not_a_xsimlab_model_input'] = 1 - assert 'add__offset' not in ds - assert sorted(ds.xsimlab.clock_coords) == ['clock', 'out'] - assert ds.out.attrs[self._output_vars_key] == 'roll__u_diff' + filtered_ds = in_dataset.xsimlab.filter_vars(model=simple_model) + + assert 'add__offset' not in filtered_ds + assert 'not_a_xsimlab_model_input' not in filtered_ds + assert sorted(filtered_ds.xsimlab.clock_coords) == ['clock', 'out'] + assert filtered_ds.out.attrs[self._output_vars_key] == 'roll__u_diff' def test_set_output_vars(self, model): ds = xr.Dataset() @@ -306,7 +309,11 @@ def test_output_vars(self, model): assert ds.xsimlab.output_vars == expected def test_run(self, model, in_dataset): - # safe mode False: model not cloned -> values set in original model + # safe mode True: ensure model is cloned + _ = in_dataset.xsimlab.run(model=model, safe_mode=True) + assert model.profile.__xsimlab_store__ is None + + # safe mode False: model not cloned -> original model is used _ = in_dataset.xsimlab.run(model=model, safe_mode=False) assert model.profile.u is not None diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index e199f6a4..b7a2b30d 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -457,7 +457,8 @@ def filter_vars(self, model=None): try: p_name, var_name = xr_var_name.split('__') except ValueError: - continue + # not a xsimlab model input: make sure to remove it + p_name, var_name = ('', xr_var_name) if (p_name, var_name) not in model.input_vars: drop_variables.append(xr_var_name) @@ -471,16 +472,6 @@ def filter_vars(self, model=None): return ds - def _clean_output_dataset(self, ds): - """Remove unnecessary attributes in output dataset ``ds``.""" - for clock in ds.xsimlab.output_vars: - if clock is None: - attrs = ds.attrs - else: - attrs = ds[clock].attrs - - attrs.pop(self._output_vars_key) - def run(self, model=None, safe_mode=True): """Run the model. @@ -509,10 +500,7 @@ def run(self, model=None, safe_mode=True): driver = XarraySimulationDriver(self._ds, model, store, output_store) - out_ds = driver.run_model() - self._clean_output_dataset(out_ds) - - return out_ds + return driver.run_model() def run_multi(self): """Run multiple models. From b7488abb384e0a633908ded5c5911aed096c08a1 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 15:53:06 +0200 Subject: [PATCH 81/97] fix tests for python < 3.6 (dict order) --- xsimlab/tests/conftest.py | 4 ++-- xsimlab/tests/fixture_model.py | 28 +++++++++++++--------------- xsimlab/tests/test_formatting.py | 4 ++-- xsimlab/tests/test_model.py | 11 ++++------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/xsimlab/tests/conftest.py b/xsimlab/tests/conftest.py index b38f131b..105fcce6 100644 --- a/xsimlab/tests/conftest.py +++ b/xsimlab/tests/conftest.py @@ -3,5 +3,5 @@ in_var_details, processes_with_store, example_process_in_model_repr) from xsimlab.tests.fixture_model import (in_dataset, out_dataset, - no_init_model, model, model_repr, - simple_model) + no_init_model, model, + simple_model, simple_model_repr) diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py index 082416c9..ce67d890 100644 --- a/xsimlab/tests/fixture_model.py +++ b/xsimlab/tests/fixture_model.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from textwrap import dedent import numpy as np @@ -80,19 +81,6 @@ def model(): 'init_profile': InitProfile}) -@pytest.fixture(scope='session') -def model_repr(): - return dedent("""\ - - init_profile - n_points [in] nb. of profile points - roll - shift [in] shift profile by a nb. of points - add - offset [in] offset added to profile u - profile""") - - @pytest.fixture def no_init_model(): return xs.Model({'roll': Roll, @@ -106,6 +94,16 @@ def simple_model(): 'profile': Profile}) +@pytest.fixture(scope='session') +def simple_model_repr(): + return dedent("""\ + + roll + shift [in] shift profile by a nb. of points + profile + u [inout] ('x',) quantity u""") + + @pytest.fixture def in_dataset(): clock_key = SimlabAccessor._clock_key @@ -122,8 +120,8 @@ def in_dataset(): (), 5, {'description': 'nb. of profile points'}) ds['roll__shift'] = ( (), 1, - {'description': 'shift profile by a nb. of points', - 'units': 'unitless'}) + OrderedDict([('description', 'shift profile by a nb. of points'), + ('units', 'unitless')])) ds['add__offset'] = ( 'clock', [1, 2, 3, 4, 5], {'description': 'offset added to profile u'}) diff --git a/xsimlab/tests/test_formatting.py b/xsimlab/tests/test_formatting.py index f0b5bf8b..57f6e3ba 100644 --- a/xsimlab/tests/test_formatting.py +++ b/xsimlab/tests/test_formatting.py @@ -62,8 +62,8 @@ def run_step(self): assert repr_process(Dummy()) == expected -def test_model_repr(model, model_repr): - assert repr_model(model) == model_repr +def test_model_repr(simple_model, simple_model_repr): + assert repr_model(simple_model) == simple_model_repr expected = "" assert repr(xs.Model({})) == expected diff --git a/xsimlab/tests/test_model.py b/xsimlab/tests/test_model.py index 305b828e..908e94f3 100644 --- a/xsimlab/tests/test_model.py +++ b/xsimlab/tests/test_model.py @@ -133,11 +133,8 @@ def test_input_vars_dict(self, model): def test_clone(self, model): cloned = model.clone() - zprocesses = zip(cloned.items(), model.items()) - - for (c_p_name, c_p_obj), (p_name, p_obj) in zprocesses: - assert c_p_name == p_name - assert c_p_obj is not p_obj + for p_name in model: + assert cloned[p_name] is not model[p_name] def test_update_processes(self, no_init_model, model): m = no_init_model.update_processes({'add': AddOnDemand, @@ -166,5 +163,5 @@ def test_visualize(self, model): show_only_variable=('profile', 'u')) assert isinstance(result, ipydisp.Image) - def test_repr(self, model, model_repr): - assert repr(model) == model_repr + def test_repr(self, simple_model, simple_model_repr): + assert repr(simple_model) == simple_model_repr From ef92931a225d4c272e646022b928fa6b3a1a4bb5 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 15:54:10 +0200 Subject: [PATCH 82/97] drop py34 support and require xarray >= 0.10.0 Use some features added in xarray 0.10.0. Conda packages are not been built anymore (both conda-forge and default channels) for a while (older versions of some packages not available in conda for py34). --- .travis.yml | 2 -- ci/requirements-py34.yml | 12 ------------ setup.py | 4 ++-- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 ci/requirements-py34.yml diff --git a/.travis.yml b/.travis.yml index 076c28fe..cc2a0edd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,6 @@ notifications: matrix: fast_finish: True include: - - python: 3.4 - env: CONDA_ENV=py34 - python: 3.5 env: CONDA_ENV=py35 - python: 3.6 diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml deleted file mode 100644 index fac39a68..00000000 --- a/ci/requirements-py34.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: test_env_py34 -channels: - - conda-forge -dependencies: - - attrs - - python=3.4 - - pytest - - numpy - - xarray - - pip: - - coveralls - - pytest-cov diff --git a/setup.py b/setup.py index 5f7222ee..9137bd50 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ packages=find_packages(), long_description=(open('README.rst').read() if exists('README.rst') else ''), - python_requires='>=3.4', - install_requires=['attrs', 'numpy', 'xarray >= 0.8.0'], + python_requires='>=3.5', + install_requires=['attrs', 'numpy', 'xarray >= 0.10.0'], tests_require=['pytest >= 3.3.0'], zip_safe=False) From a9267dbb5de19d86c748a845f80511a71932fd28 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 17:19:48 +0200 Subject: [PATCH 83/97] update travis conf (add xarray and attrs dev to test matrix) --- .travis.yml | 14 ++++++++++++-- ci/requirements-py36-attrs-dev.yml | 12 ++++++++++++ ci/requirements-py36-xarray-dev.yml | 12 ++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 ci/requirements-py36-attrs-dev.yml create mode 100644 ci/requirements-py36-xarray-dev.yml diff --git a/.travis.yml b/.travis.yml index cc2a0edd..efeba4b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,15 @@ matrix: env: CONDA_ENV=py35 - python: 3.6 env: CONDA_ENV=py36 + - python: 3.6 + env: CONDA_ENV=py36-xarray-dev + - python: 3.6 + env: CONDA_ENV=py36-attr-dev + allow_failures: + - python: 3.6 + env: CONDA_ENV=py36-xarray-dev + - python: 3.6 + env: CONDA_ENV=py36-attrs-dev before_install: - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then @@ -29,10 +38,11 @@ before_install: install: - conda env create --file ci/requirements-$CONDA_ENV.yml - source activate test_env_$CONDA_ENV - - python setup.py install + - conda list + - pip install --no-deps -e . script: - - py.test xsimlab --cov=xsimlab --cov-report term-missing + - py.test xsimlab --cov=xsimlab --cov-report term-missing --verbose after_success: - coveralls diff --git a/ci/requirements-py36-attrs-dev.yml b/ci/requirements-py36-attrs-dev.yml new file mode 100644 index 00000000..1a1b7a30 --- /dev/null +++ b/ci/requirements-py36-attrs-dev.yml @@ -0,0 +1,12 @@ +name: test_env_py36 +channels: + - conda-forge +dependencies: + - python=3.6 + - pytest + - numpy + - xarray + - pip: + - git+https://github.com/python-attrs/attrs.git + - coveralls + - pytest-cov diff --git a/ci/requirements-py36-xarray-dev.yml b/ci/requirements-py36-xarray-dev.yml new file mode 100644 index 00000000..3af0f6ba --- /dev/null +++ b/ci/requirements-py36-xarray-dev.yml @@ -0,0 +1,12 @@ +name: test_env_py36 +channels: + - conda-forge +dependencies: + - attrs + - python=3.6 + - pytest + - numpy + - pip: + - git+https://github.com/pydata/xarray.git + - coveralls + - pytest-cov From f83d2d00a6eb5a334511beea7cae709ab355f469 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 17:23:41 +0200 Subject: [PATCH 84/97] fix travis conf --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index efeba4b1..39385053 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - python: 3.6 env: CONDA_ENV=py36-xarray-dev - python: 3.6 - env: CONDA_ENV=py36-attr-dev + env: CONDA_ENV=py36-attrs-dev allow_failures: - python: 3.6 env: CONDA_ENV=py36-xarray-dev From 3886cbdc68d07a71dd06203b30abf675c74aacf7 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Thu, 3 May 2018 17:35:58 +0200 Subject: [PATCH 85/97] fix travis take 2 (fix conda env name in dev tests) --- ci/requirements-py36-attrs-dev.yml | 2 +- ci/requirements-py36-xarray-dev.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/requirements-py36-attrs-dev.yml b/ci/requirements-py36-attrs-dev.yml index 1a1b7a30..9909a83f 100644 --- a/ci/requirements-py36-attrs-dev.yml +++ b/ci/requirements-py36-attrs-dev.yml @@ -1,4 +1,4 @@ -name: test_env_py36 +name: test_env_py36-attrs-dev channels: - conda-forge dependencies: diff --git a/ci/requirements-py36-xarray-dev.yml b/ci/requirements-py36-xarray-dev.yml index 3af0f6ba..4e6fb640 100644 --- a/ci/requirements-py36-xarray-dev.yml +++ b/ci/requirements-py36-xarray-dev.yml @@ -1,4 +1,4 @@ -name: test_env_py36 +name: test_env_py36-xarray-dev channels: - conda-forge dependencies: From 536f083c62df356956880a0642e585d368f61dfd Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 4 May 2018 16:10:10 +0200 Subject: [PATCH 86/97] update doc (framework and api) --- doc/api.rst | 125 ++++----------------------- doc/conf.py | 6 ++ doc/framework.rst | 192 +++++++++++++++++++---------------------- xsimlab/process.py | 13 ++- xsimlab/xr_accessor.py | 2 - 5 files changed, 120 insertions(+), 218 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 4a975e8c..433bca23 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -79,13 +79,17 @@ Model introspection ------------------- ``Model`` implements an immutable mapping interface where keys are -process names and values are objects of ``Process`` subclasses (attribute-style -access is also supported). +process names and values are objects of ``Process`` subclasses +(attribute-style access is also supported). .. autosummary:: :toctree: _api_generated/ + Model.all_vars + Model.all_vars_dict Model.input_vars + Model.input_vars_dict + Model.dependent_processes Model.visualize Running a model @@ -108,126 +112,31 @@ interfaces. Process ======= -Note: ``Process`` is a base class that should be subclassed. +Creating a process +------------------ .. autosummary:: :toctree: _api_generated/ - Process + process -Clone a process ---------------- - -.. autosummary:: - :toctree: _api_generated/ - - Process.clone - -Process interface and introspection +Process introspection and variables ----------------------------------- -``Process`` implements an immutable mapping interface where keys are -variable names and values are Variable objects (attribute-style -access is also supported). - .. autosummary:: :toctree: _api_generated/ - Process.variables - Process.meta - Process.name - Process.info - -Process "abstract" methods --------------------------- - -Subclasses of ``Process`` usually implement at least some of the methods below. - -.. autosummary:: - :toctree: _api_generated/ - - Process.validate - Process.initialize - Process.run_step - Process.finalize_step - Process.finalize + process_info + variable_info + filter_variables Variable ======== -Base variable class -------------------- - -Although it has the same name, this class is different from -:py:class:`xarray.Variable`. - -.. autosummary:: - :toctree: _api_generated/ - - Variable - -**Attributes** - -.. autosummary:: - :toctree: _api_generated/ - - Variable.value - Variable.state - Variable.rate - Variable.change - -**Methods** - -.. autosummary:: - :toctree: _api_generated/ - - Variable.to_xarray_variable - -Derived variable classes ------------------------- - -These classes inherit from ``Variable``. - -.. autosummary:: - :toctree: _api_generated/ - - NumberVariable - FloatVariable - IntegerVariable - -Foreign variable ----------------- - -.. autosummary:: - :toctree: _api_generated/ - - ForeignVariable - -**Attributes** - -.. autosummary:: - :toctree: _api_generated/ - - ForeignVariable.ref_process - ForeignVariable.ref_var - ForeignVariable.value - ForeignVariable.state - ForeignVariable.rate - ForeignVariable.change - -Diagnostic variable -------------------- - -.. autosummary:: - :toctree: _api_generated/ - - diagnostic - -Collections of variables ------------------------- - .. autosummary:: :toctree: _api_generated/ - VariableList - VariableGroup + variable + foreign + group + on_demand diff --git a/doc/conf.py b/doc/conf.py index c5d8d913..f2a3a47d 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -26,6 +26,11 @@ print("numpy: %s, %s" % (numpy.__version__, numpy.__file__)) except ImportError: print("no numpy") +try: + import attr + print("attr: %s, %s" % (attr.__version__, attr.__file__)) +except ImportError: + print("no attr") try: import xarray print("xarray: %s, %s" % (xarray.__version__, xarray.__file__)) @@ -205,6 +210,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3.6/', None), 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'attr': ('http://www.attrs.org/en/stable/', None), 'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None), 'xarray': ('http://xarray.pydata.org/en/stable/', None) } diff --git a/doc/framework.rst b/doc/framework.rst index 185d6f56..e97513d4 100644 --- a/doc/framework.rst +++ b/doc/framework.rst @@ -40,8 +40,8 @@ The Model class also implements specific methods for: Processes --------- -Processes are defined as custom Python classes that inherit from the -base class :class:`~xsimlab.Process`. The role of a process is twofold: +Processes are defined as Python classes that are decorated by +:func:`~xsimlab.process`. The role of a process is twofold: - declare a given subset of the variables used in a model, - define a specific set of instructions that use or compute values for @@ -52,41 +52,52 @@ model. It may for example represent a particular physical mechanism that is described in terms of one or more state variables (e.g., scalar or vector fields) and one or more operations -- with or without parameters -- that modify those state variables through time. Note -that some processes may be time-independent. +that some processes may be time-independent or may even be used to +declare variables without implementing any computation. .. note:: xarray-simlab does not provide any built-in logic for tasks like generating computational meshes or setting boundary conditions, which should rather be implemented in 3rd-party libraries as - time-independent processes. Even those tasks may be too specialized - to justify including them in this framework, which aims to be as - general as possible. + processes. Even those tasks may be too specialized to justify + including them in this framework, which aims to be as general as + possible. + +A process-ified class behaves mostly like any other regular Python +class, i.e., there is a-priori nothing that prevents us from using +the common object-oriented features as you like. The only difference +is that you can here create classes in a very succinct way without +boilerplate, i.e., you don't need to implement dunder methods like +``__init__`` or ``__repr__`` as this is handled by the framework. In +fact, this framework uses and extends the attrs_ package: +:func:`~xsimlab.process` is a wrapper around :func:`attr.s` and the +functions used to create variables (see below) are thin wrappers +around :func:`attr.ib`. + +.. _attrs: http://www.attrs.org Variables --------- -Variables are the most basic elements of a model. They consist of -:class:`~xsimlab.Variable` [*]_ objects that are declared in processes as class -attributes. They have the following properties: +Variables are the most basic elements of a model. They are declared in +processes as class attributes, using :func:`~xsimlab.variable`. It +allows to define useful metadata such as: -- data values (state, rate or change -- see below), -- validators, i.e., callables for checking supplied data values, - labeled dimensions (or no dimension for scalars), -- predefined meta-data attributes like description or default value, -- user-defined meta-data attributes (e.g., units, math symbol). +- predefined meta-data attributes, e.g., a short description, +- user-defined meta-data attributes, e.g., units or math symbol, +- the intent for a variable, i.e., whether the process + needs (``intent='in'``), updates (``intent='inout'``) or computes + (``intent='out'``) a value for that variable. .. note:: - xarray-simlab does not distinguish between model parameters - and state variables. Both are declared as Variable objects. - -.. [*] usually variables consist of objects of derived classes like, - e.g., ``FloatVariable`` or ``IntegerVariable`` depending on their - expected value type. + xarray-simlab does not distinguish between model parameters, input + and output variables. All can be declared using ``variable``. Foreign variables ------------------ +~~~~~~~~~~~~~~~~~ Like different physical mechanisms involve some common state variables (e.g., temperature or pressure), different processes may operate on @@ -94,11 +105,10 @@ common variables. In xarray-simlab, a variable is declared at a unique place, i.e., within one and only one process. Using common variables across -processes is achieved by declaring foreign variables. -:class:`~xsimlab.ForeignVariable` objects are references to -variables that are declared in other processes ; it allows getting or -setting values just as if these references were the original -variables. +processes is achieved by declaring :func:`~xsimlab.foreign` +variables. These are simply references to variables that are declared +in other processes. You can use it for any computation inside a process +just like original variables. The great advantage of declaring variables at unique places is that all their meta-data are defined once. However, a downside of this @@ -107,61 +117,36 @@ links between processes, which makes harder reusing these processes independently of each other. Variable groups ---------------- +~~~~~~~~~~~~~~~ -In some cases, using variables groups may provide an elegant +In some cases, using variable groups may provide an elegant alternative to hard-coded links between processes. -The membership of Variable objects to a group is defined via their -``group`` attribute. If we want to use in a separate process all the -variables of a group, instead of explicitly declaring foreign -variables we can declare a :class:`~xsimlab.VariableGroup` object -which behaves like an iterable of ForeignVariable objects pointing to -each of the variables of the group. +The membership of variables to a group is defined via their ``group`` +attribute. If you want to use in a separate process all the variables +of a group, instead of explicitly declaring foreign variables you can +declare a :func:`~xsimlab.group` variable. The latter behaves like an +iterable of foreign variables pointing to each of the variables that +are members of the group, across the model. -Variable groups are useful particularly in cases where we want to +Variable groups are useful particularly in cases where you want to combine different processes that act on the same variable, e.g. in landscape evolution modeling combine the effect of different erosion -processes on the evolution of the surface elevation. This way we can +processes on the evolution of the surface elevation. This way you can easily add or remove processes to/from a model and avoid missing or broken links between processes. -Variable state, rate and change -------------------------------- - -A single variable may accept up to 3 concurrent values that each -have a particular meaning: - -- a state, i.e., the value of the variable at a given time, -- a rate, i.e., the value of the time-derivative at a given time, -- a change, i.e., the value of the time-derivative integrated for a given - time step. - -These are accessible as properties of Variable and ForeignVariable -objects: ``state``, ``rate`` and ``change``. An additional property -``value`` is defined as an alias of ``state``. - -.. note:: - - These properties are for convenience only, it avoids having to - duplicate Variable objects for state variables. The properties are - independent of each other and their given meanings serve only as - conventions. Although there is no restriction in using any of these - properties anywhere in a process, it is good practice to follow - these conventions. - -.. note:: - - The ``rate`` and ``change`` properties should never be used for - variables other than state variables. Moreover, it is preferable - to use the alias ``value`` instead of ``state`` as the latter is - quite meaningless in this case. +On-demand variables +~~~~~~~~~~~~~~~~~~~ -.. todo_move_this_elsewhere +On-demand variables are like regular variables, except that their +value is not intended to be computed systematically, e.g., at each +time step of a simulation, but instead only at a given few +times. These are declared using :func:`~xsimlab.on_demand` and must +implement in the same process-ified class a dedicated method that +computes their value. - For state variables, a common practice is to compute ``rate`` or - ``change`` values during the "run step" stage and update ``state`` - values during the "finalize step" stage. +These variables are useful, e.g., for model diagnostics. Simulation workflow ------------------- @@ -173,35 +158,53 @@ A model run is divided into four successive stages: 3. finalize step 4. finalization -During a simulation, stages 1 and 4 are run only once while steps 2 +During a simulation, stages 1 and 4 are run only once while stages 2 and 3 are repeated for a given number of (time) steps. -Each process provides its own computation instructions for those -stages. Note that this is optional, except for time-dependent -processes that must provide some instructions at least for stage 2 -(run step). For time-independent processes stages 2 and 3 are ignored. +Each process-ified class may provide its own computation instructions +for those stages by implementing specific methods (one per +stage). Note that this is entirely optional. For example, +time-independent processes (e.g., for setting model grids) usually +implement stage 1 only. In a few cases, the role of a process may even +consist of just declaring some variables that are used elsewhere. + +Get / set variable values inside a process +------------------------------------------ + +Once you have declared a variable as a class attribute in a process, you +can further get and/or set its value like it was defined as a property +of that class. For example, if you declare a variable ``foo`` you can +just use ``self.foo`` to get/set its value inside one method of that +class. + +This is exactly what does the :func:`~xsimlab.process` decorator: it +takes all variables declared as class attributes and turns them into +properties, which may be read-only depending on the ``intent`` set for +the variables. + +Basically, these properties read/write values from/into a simple +key-value store (except for on-demand variables). Currently the store +is fully in-memory but it could be easily replaced by an on-disk or a +distributed store. The xarray-simlab's modeling framework can thus be +viewed as a thin object-oriented layer built on top of an abstract +key-value store. Process dependencies and ordering --------------------------------- The order in which processes are executed during a simulation is -critical. For example, if the role of a process is to provide a value +critical. For example, if the role of a process is to compute a value for a given variable, then the execution of this process must happen before the execution of all other processes that use the same variable in their computation. -Such role can be defined using the ``provided`` attribute of Variable -and ForeignVariable objects, which is either set to True or False -(note that a process may still update a variable value even if -``provided`` is set to False, see Model inputs section below). - In a model, the processes and their dependencies together form the nodes and the edges of a Directed Acyclic Graph (DAG). The graph -topology is fully determined by the role set for each variable or -foreign variable declared in each process. An ordering that is +topology is fully determined by the ``intent`` set for each variable +or foreign variable declared in each process. An ordering that is computationally consistent can then be obtained using topological -sorting. This is done at Model object creation. The same ordering -is used at every stage of a model run. +sorting. This is done at Model object creation. The same ordering is +used at every stage of a model run. In principle, the DAG structure would also allow running the processes in parallel at every stage of a model run. This is not yet @@ -214,20 +217,7 @@ In a model, inputs are variables that need a value to be set by the user before running a simulation. Like process ordering, inputs are automatically retrieved at Model -object creation, using the ``provided`` attribute of Variable and -ForeignVariable objects. Inputs are Variable objects for which -``provided`` is set to False and which don't have any linked -ForeignVariable object with ``provided`` set to True. - -.. note:: - - Any value required as model input relates to the ``state`` property - (or its alias ``value``) of a Variable object. The ``rate`` and - ``change`` properties should never be set by model users, like any - property of ForeignVariable objects. - -.. move_this_foreign_variable - - ForeignVariable.state return the same object (usually a numpy array) than - Variable.state (replace class names by variable names in processes). - ForeignVariable.state is actually a shortcut to ForeignVariable.ref_var.state. +object creation by looking at the ``intent`` set for all variables and +foreign variables in the model. A variable is a model input if it has +``intent`` set to ``'in'`` or ``'inout'`` and if it has no linked +foreign variable with ``intent='out'``. diff --git a/xsimlab/process.py b/xsimlab/process.py index a0cfdd7c..88a8dfa8 100644 --- a/xsimlab/process.py +++ b/xsimlab/process.py @@ -329,8 +329,8 @@ def process(maybe_cls=None, autodoc=False): :func:`group`). This decorator automatically adds properties to get/set values for these variables. - - One or more methods among `initialize()`, `run_step()`, - `finalize_step()` and `finalize()`, which are called at different + - One or more methods among ``initialize()``, ``run_step()``, + ``finalize_step()`` and ``finalize()``, which are called at different stages of a simulation and perform some computation based on the variables defined in the process interface. @@ -340,12 +340,11 @@ def process(maybe_cls=None, autodoc=False): Parameters ---------- maybe_cls : class, optional - Allows to apply this decorator to a class either as `@process` or - `@process(*args)`. + Allows to apply this decorator to a class either as ``@process`` or + ``@process(*args)``. autodoc : bool, optional - If True, render the docstrings given as a template and fill the - corresponding sections with metadata found in the class - (default: False). + If True, render the docstrings template and fill the + corresponding sections with variable metadata (default: False). """ def wrap(cls): diff --git a/xsimlab/xr_accessor.py b/xsimlab/xr_accessor.py index b7a2b30d..a4696129 100644 --- a/xsimlab/xr_accessor.py +++ b/xsimlab/xr_accessor.py @@ -2,8 +2,6 @@ xarray extensions (accessors). """ -from collections import defaultdict - import numpy as np from xarray import as_variable, Dataset, register_dataset_accessor From 2c5ca50c1d1a8a9853cc282b447313cde8c9ca61 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 10:56:11 +0200 Subject: [PATCH 87/97] add line return at the end of all repr --- xsimlab/formatting.py | 18 ++++++++++-------- xsimlab/tests/fixture_model.py | 3 ++- xsimlab/tests/fixture_process.py | 9 ++++++--- xsimlab/tests/test_formatting.py | 5 +++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/xsimlab/formatting.py b/xsimlab/formatting.py index c1d4a80f..9e0daf18 100644 --- a/xsimlab/formatting.py +++ b/xsimlab/formatting.py @@ -95,7 +95,7 @@ def var_details(var): details = "\n".join(["- {} : {}".format(k, v) for k, v in detail_items]) - return description + "\n\n" + details + return description + "\n\n" + details + '\n' def repr_process(process): @@ -132,11 +132,13 @@ def repr_process(process): else: stages_section_details = " *no stage implemented*" - return "\n".join([header, - var_section_summary, - var_section_details, - stages_section_summary, - stages_section_details]) + process_repr = "\n".join([header, + var_section_summary, + var_section_details, + stages_section_summary, + stages_section_details]) + + return process_repr + '\n' def repr_model(model): @@ -146,7 +148,7 @@ def repr_model(model): .format(n_processes, len(model.input_vars))) if not n_processes: - return header + return header + '\n' col_width = _calculate_col_width( [var_name for _, var_name in model.input_vars] @@ -169,4 +171,4 @@ def repr_model(model): sections.append(p_section) - return header + '\n' + '\n'.join(sections) + return header + '\n' + '\n'.join(sections) + '\n' diff --git a/xsimlab/tests/fixture_model.py b/xsimlab/tests/fixture_model.py index ce67d890..ec9d75e7 100644 --- a/xsimlab/tests/fixture_model.py +++ b/xsimlab/tests/fixture_model.py @@ -101,7 +101,8 @@ def simple_model_repr(): roll shift [in] shift profile by a nb. of points profile - u [inout] ('x',) quantity u""") + u [inout] ('x',) quantity u + """) @pytest.fixture diff --git a/xsimlab/tests/fixture_process.py b/xsimlab/tests/fixture_process.py index 2e5b5619..e959798a 100644 --- a/xsimlab/tests/fixture_process.py +++ b/xsimlab/tests/fixture_process.py @@ -67,7 +67,8 @@ def example_process_repr(): in_foreign_od_var [in] <--- SomeProcess.some_od_var group_var [in] <--- group 'some_group' Simulation stages: - *no stage implemented*""") + *no stage implemented* + """) @pytest.fixture(scope='session') @@ -79,7 +80,8 @@ def in_var_details(): - intent : in - dims : (('x',), ('x', 'y')) - group : None - - attrs : {}""") + - attrs : {} + """) def _init_process(p_cls, p_name, model, store, store_keys=None, od_keys=None): @@ -145,4 +147,5 @@ def example_process_in_model_repr(): in_foreign_od_var [in] <--- some_process.some_od_var group_var [in] <--- group 'some_group' Simulation stages: - *no stage implemented*""") + *no stage implemented* + """) diff --git a/xsimlab/tests/test_formatting.py b/xsimlab/tests/test_formatting.py index 57f6e3ba..57049332 100644 --- a/xsimlab/tests/test_formatting.py +++ b/xsimlab/tests/test_formatting.py @@ -57,7 +57,8 @@ def run_step(self): *empty* Simulation stages: initialize - run_step""") + run_step + """) assert repr_process(Dummy()) == expected @@ -65,5 +66,5 @@ def run_step(self): def test_model_repr(simple_model, simple_model_repr): assert repr_model(simple_model) == simple_model_repr - expected = "" + expected = "\n" assert repr(xs.Model({})) == expected From cb5e2d6865925208acf9a498e20d4cd72c31b875 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 11:55:01 +0200 Subject: [PATCH 88/97] fix xarray simulation driver (avoid xarray objects for time steps) --- xsimlab/drivers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xsimlab/drivers.py b/xsimlab/drivers.py index 69dd8e81..6d6fe721 100644 --- a/xsimlab/drivers.py +++ b/xsimlab/drivers.py @@ -155,7 +155,7 @@ def _get_time_steps(self): """Return a xarray.DataArray of duration between two consecutive time steps.""" mclock = self.dataset[self.master_clock_dim] - return mclock.diff(self.master_clock_dim) + return mclock.diff(self.master_clock_dim).values def _split_clock_inputs(self): """Return two datasets with time-independent and time-dependent @@ -246,12 +246,12 @@ def run_model(self): ds_in, ds_in_clock = self._split_clock_inputs() has_clock_inputs = bool(ds_in_clock.data_vars) - da_dt = self._get_time_steps() + dt_array = self._get_time_steps() self._set_input_vars(ds_in) self.model.initialize() - for istep, dt in enumerate(da_dt): + for istep, dt in enumerate(dt_array): if has_clock_inputs: ds_in_step = ds_in_clock.isel(**{self.master_clock_dim: istep}) self._set_input_vars(ds_in_step) From 7143027f489f3b5e608b5d42b4d8020fb7339b14 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 11:56:13 +0200 Subject: [PATCH 89/97] wip update doc (update all user guide sections) --- doc/create_model.rst | 211 +++++++++++++++------------------ doc/framework.rst | 70 ++++++----- doc/inspect_model.rst | 82 ++++++++----- doc/run_model.rst | 35 +++--- doc/scripts/advection_model.py | 150 +++++++++++------------ doc/whats_new.rst | 4 + 6 files changed, 283 insertions(+), 269 deletions(-) diff --git a/doc/create_model.rst b/doc/create_model.rst index 2046b6ad..a093f5a9 100644 --- a/doc/create_model.rst +++ b/doc/create_model.rst @@ -30,7 +30,7 @@ between times :math:`n` and :math:`n+1`. We could just implement this numerical model with a few lines of Python / Numpy code, e.g., here below assuming periodic boundary -conditions and a gaussian pulse as initial profile. We will show, +conditions and a Gaussian pulse as initial profile. We will show, however, that it is very easy to refactor this code for using it with xarray-simlab. We will also show that, while enabling useful features, the refactoring still results in a short amount of readable code. @@ -42,9 +42,9 @@ the refactoring still results in a short amount of readable code. Anatomy of a Process subclass ----------------------------- -Let's first wrap the code above into a single subclass of -:class:`~xsimlab.Process` named ``AdvectionLax1D``. Next we'll explain in -detail the content of this class. +Let's first wrap the code above into a single class named +``AdvectionLax1D`` decorated by :class:`~xsimlab.process`. Next we'll +explain in detail the content of this class. .. literalinclude:: scripts/advection_model.py :lines: 3-32 @@ -54,44 +54,35 @@ Process interface ``AdvectionLax1D`` has some class attributes declared at the top, which together form the process' "public" interface, i.e., all the -variables that we want to be publicly exposed by this process. These -attributes usually correspond to instances of -:class:`~xsimlab.Variable` or derived classes, depending on their -expected value type, like :class:`~xsimlab.FloatVariable` in this case -(see section :doc:`api` for a full list of available classes). - -The creation of Variable objects requires to explicitly provide -dimension label(s) for arrays or an empty tuple for scalars. In this -case, variables ``spacing``, ``length``, ``loc`` and ``scale`` are all -scalars, whereas ``x`` and ``u`` are both arrays defined on the -1-dimensional :math:`x` grid. Multiple choices can also be given as a -list, like variable ``v`` which represents a velocity field that can -be either constant (scalar) or variable (array) in space. +variables that we want to be publicly exposed by this process. Here we +use :func:`~xsimlab.variable` to add some metadata to each variable +of the interface. + +We first may specify the labels of the dimensions expected for each +variable, which defaults to an empty tuple (i.e., a scalar value is +expected). In this example, variables ``spacing``, ``length``, ``loc`` +and ``scale`` are all scalars, whereas ``x`` and ``u`` are both arrays +defined on the 1-dimensional :math:`x` grid. Multiple choices can also +be given as a list, like variable ``v`` which represents a velocity +field that can be either constant (scalar) or variable (array) in +space. .. note:: - All variable objects also implicitly allow a time dimension as - well as their own dimension (coordinate). See section :doc:`run_model`. + All variable objects also implicitly allow a time dimension. + See section :doc:`run_model`. -There is also a set of common arguments available to all Variable -types. All are optional. In the example above, ``description`` and -``attrs`` are used to define some (custom) metadata. +Additionally, it is also possible to add a short ``description`` +and/or custom metadata like units with the ``attrs`` argument. -Variables ``x`` and ``u`` have also an option ``provided`` set to -``True``. It means that the process ``AdvectionLax1D`` itself provides -a value for these variables. ``provided=False`` (default) means -that a value should be provided elsewhere, either by another process -or as model input. - -.. note:: - - A process which updates the value (i.e., state) of a variable - during a simulation does not necessarily imply setting - ``provide=True`` for that variable, e.g., when it still requires an - initial value. - -Other options are available, see :class:`~xsimlab.Variable` for full -reference. +Another important argument is ``intent``, which specifies how the +process deals with the value of the variable. By default, +``intent='in'`` means that the process just needs the value of the +variable for its computation ; this value should either be computed +elsewhere by another process or be provided by the user as model +input. By contrast, variables ``x`` and ``u`` have ``intent='out'``, +which means that the process ``AdvectionLax1D`` itself initializes and +computes a value for these two variables. Process "runtime" methods ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -99,62 +90,56 @@ Process "runtime" methods Beside its interface, the process ``AdvectionLax1D`` also implements methods that will be called during simulation runtime: -- ``initialize`` will be called once at the beginning of a +- ``.initialize()`` will be called once at the beginning of a simulation. Here it is used to set the x-coordinate values of the - grid and the initial values of ``u`` along the grid (gaussian + grid and the initial values of ``u`` along the grid (Gaussian pulse). -- ``run_step`` will be called at each time step iteration and have the - current time step duration as required argument. This is where the - Lax method is implemented. -- ``finalize_step`` will be called at each time step iteration too but - after having called ``run_step`` for all other processes (if +- ``.run_step()`` will be called at each time step iteration and have + the current time step duration as required argument. This is where + the Lax method is implemented. +- ``.finalize_step()`` will be called at each time step iteration too + but after having called ``run_step`` for all other processes (if any). Its intended use is mainly to ensure that state variables like ``u`` are updated consistently and after having taken snapshots. -A fourth method ``finalize`` could also be implemented, but it is not -needed in this case. This method is called once at the end of the -simulation, e.g., for cleaning purposes. +A fourth method ``.finalize()`` could also be implemented, but it is +not needed in this case. This method is called once at the end of the +simulation, e.g., for some clean-up. -Accessing process variables and values -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Getting / setting variable values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The Variable objects declared in ``AdvectionLax1D`` can be accessed -elsewhere in the class like normal attributes, e.g., using ``self.u`` -for variable ``u``. +For each variable declared as class attributes in ``AdvectionLax1D`` +we can get their value (and/or set a value depending on their +``intent``) elsewhere in the class like if it was defined as regular +instance attributes, e.g., using ``self.u`` for variable ``u``. .. note:: - Like the other variables, ``self.u`` actually returns a copy of the - corresponding ``FloatVariable`` object that is originally declared - as a class attribute. Some internal magic happens in xarray-simlab - in order to avoid value conflicts when using the same process in - different contexts. + In xarray-simlab it is safe to run multiple simulations + concurrently: each simulation has its own process instances. -Variable objects may hold multiple, independent values that we set/get -via specific properties (see section :doc:`framework`), e.g., -``self.u.state`` for :math:`u` values and ``self.x.value`` for -x-coordinate values on the grid. Note that we use here the property -``value`` for all time-independent variables, which is just an alias -of ``state`` (this is purely conventional). - -Beside Variable object attributes, we can of course use normal -attributes in Process subclasses too, like ``self.u1`` in -``AdvectionLax1D``. +Beside variables declared in the process interface, nothing prevent us +from using regular attributes in process classes if needed. For +example, ``self.u1`` is set as a temporary internal state in +``AdvectionLax1D`` to wait for the "finalize step" stage before +updating :math:`u`. Creating a Model instance ------------------------- Creating a new :class:`~xsimlab.Model` instance is very easy. We just -need to provide a dictionary with the process(es) that we want to -include in the model, e.g., with only the process created above: +need to provide a dictionary with the process class(es) that we want +to include in the model, e.g., with only the process created above: .. literalinclude:: scripts/advection_model.py - :lines: 35-38 + :lines: 35 -That's it! Now we can use that model with the xarray extension provided -by xarray-simlab to create new setups, run the model, take snapshots -for one or more variables on a given frequency, etc. (see section -:doc:`run_model`). +That's it! Now we have different tools already available to inspect +the model (see section :doc:`inspect_model`). We can also use that +model with the xarray extension provided by xarray-simlab to create +new setups, run the model, take snapshots for one or more variables on +a given frequency, etc. (see section :doc:`run_model`). Fine-grained process refactoring -------------------------------- @@ -164,9 +149,9 @@ the initial conditions? Use a grid with variable spacing? Add another physical process impacting :math:`u` such as a source or sink term? In all cases we would need to modify the class ``AdvectionLax1D``. -This framework works best if instead we first split the problem into -small pieces, i.e., small Process subclasses that we can easily -combine and replace in models. +This framework works best if we instead split the problem into small +pieces, i.e., small process classes that we can easily combine and +replace in models. The ``AdvectionLax1D`` process may for example be refactored into 4 separate processes: @@ -183,57 +168,57 @@ This process declares all grid-related variables and computes x-coordinate values. .. literalinclude:: scripts/advection_model.py - :lines: 41-52 + :lines: 38-47 -``class Meta`` is used here to specify that this process is not time -dependent (by default processes are considered as -time-dependent). Grid x-coordinate values only need to be set once at -the beginning of the simulation ; there is no need to implement -``run_step`` here. +Grid x-coordinate values only need to be set once at the beginning of +the simulation ; there is no need to implement ``.run_step()`` here. **ProfileU** .. literalinclude:: scripts/advection_model.py - :lines: 55-69 + :lines: 50-62 -``u_vars`` is declared as a :class:`~xsimlab.VariableGroup`, i.e., an +``u_vars`` is declared as a :func:`~xsimlab.group` variable, i.e., an iterable of all variables declared elsewhere that belong the same -group ('u_vars' in this case). In this case, it allows to further add -one or more processes that would also impact :math:`u` in addition to -advection. +group ('u_vars' in this case). In this example, it allows to further +add one or more processes that will also affect the evolution of +:math:`u` in addition to advection (see below). + +Note also ``intent='inout'`` set for ``u``, which means that +``ProfileU`` updates the value of :math:`u` but still needs an initial +value from elsewhere. **AdvectionLax** .. literalinclude:: scripts/advection_model.py - :lines: 72-92 + :lines: 65-83 ``u_advected`` represents the effect of advection on the evolution of -:math:`u` and therefore belongs to the group 'u_vars'. By convention -we use the property ``change`` to store values for that variable. - -Computing values of ``u_advected`` requires access to the values of -variables ``spacing`` and ``u`` that are already declared in the -``UniformGrid1D`` and ``ProfileU`` classes, respectively. -:class:`~xsimlab.ForeignVariable` allows to declare references to -these external variables and handle them just as if these were the -original variables. For example, ``self.grid_spacing.value`` in this -class will return the same value than ``self.spacing`` in -``UniformGrid1D``. +:math:`u` and therefore belongs to the group 'u_vars'. + +Computing values of ``u_advected`` requires values of variables +``spacing`` and ``u`` that are already declared in the +``UniformGrid1D`` and ``ProfileU`` classes, respectively. Here we +declare them as :func:`~xsimlab.foreign` variables, which allows to +handle them like if these were the original variables. For example, +``self.grid_spacing`` in this class will return the same value than +``self.spacing`` in ``UniformGrid1D``. **InitUGauss** .. literalinclude:: scripts/advection_model.py - :lines: 95-109 + :lines: 86-96 -Note that ForeignVariable can also be used to set values for variables -that are declared in other processes, as for ``u`` here. +A foreign variable can also be used to set values for variables that +are declared in other processes, as for ``u`` here with +``intent='out'``. **Refactored model** We now have all the building blocks to create a more flexible model: .. literalinclude:: scripts/advection_model.py - :lines: 112-115 + :lines: 99-102 The order in which processes are given doesn't matter (it is a dictionary). A computationally consistent order, as well as model @@ -258,9 +243,9 @@ original, simple version. For this we create a new process: .. literalinclude:: scripts/advection_model.py - :lines: 118-142 + :lines: 105-130 -A couple of comments about this class: +Some comments about this class: - ``u_source`` belongs to the group 'u_vars' and therefore will be added to ``u_advected`` in ``ProfileU`` process. @@ -270,21 +255,21 @@ A couple of comments about this class: - Nearest node index and source rate array will be recomputed at each time iteration because variables ``loc`` and ``flux`` may both have a time dimension (variable source location and intensity), i.e., - ``self.loc.value`` and ``self.flux.value`` may both change at each + ``self.loc`` and ``self.flux`` may both change at each time iteration. In this example we also want to start with a flat, zero :math:`u` -profile instead of a gaussian pulse. We create another (minimal) +profile instead of a Gaussian pulse. We create another (minimal) process for that: .. literalinclude:: scripts/advection_model.py - :lines: 145-155 + :lines: 133-141 Using one command, we can then update the model with these new features: .. literalinclude:: scripts/advection_model.py - :lines: 158-159 + :lines: 144-145 Compared to ``model2``, this new ``model3`` have a new process named 'source' and a replaced process 'init'. @@ -295,7 +280,7 @@ It is also possible to create new models by removing one or more processes from existing Model instances, e.g., .. literalinclude:: scripts/advection_model.py - :lines: 162 + :lines: 148 In this latter case, users will have to provide initial values of :math:`u` along the grid directly as an input array. @@ -304,5 +289,5 @@ In this latter case, users will have to provide initial values of Model instances are immutable, i.e., once created it is not possible to modify these instances by adding, updating or removing - processes. Both methods ``.update_processes`` and - ``.drop_processes`` always return new Model instances. + processes. Both methods ``.update_processes()`` and + ``.drop_processes()`` always return new instances of ``Model``. diff --git a/doc/framework.rst b/doc/framework.rst index e97513d4..8c86f30c 100644 --- a/doc/framework.rst +++ b/doc/framework.rst @@ -65,7 +65,7 @@ declare variables without implementing any computation. possible. A process-ified class behaves mostly like any other regular Python -class, i.e., there is a-priori nothing that prevents us from using +class, i.e., there is a-priori nothing that prevents you from using the common object-oriented features as you like. The only difference is that you can here create classes in a very succinct way without boilerplate, i.e., you don't need to implement dunder methods like @@ -81,8 +81,9 @@ Variables --------- Variables are the most basic elements of a model. They are declared in -processes as class attributes, using :func:`~xsimlab.variable`. It -allows to define useful metadata such as: +processes as class attributes, using :func:`~xsimlab.variable`. +Declaring variables mainly consists of defining useful metadata such +as: - labeled dimensions (or no dimension for scalars), - predefined meta-data attributes, e.g., a short description, @@ -94,7 +95,8 @@ allows to define useful metadata such as: .. note:: xarray-simlab does not distinguish between model parameters, input - and output variables. All can be declared using ``variable``. + and output variables. All can be declared using + :func:`~xsimlab.variable`. Foreign variables ~~~~~~~~~~~~~~~~~ @@ -107,8 +109,16 @@ In xarray-simlab, a variable is declared at a unique place, i.e., within one and only one process. Using common variables across processes is achieved by declaring :func:`~xsimlab.foreign` variables. These are simply references to variables that are declared -in other processes. You can use it for any computation inside a process -just like original variables. +in other processes. + +You can use foreign variables for almost any computation inside a +process just like original variables. The only difference is that +``intent='inout'`` is not supported for a foreign variable, i.e., a +process may either need or compute a value of a foreign variable but +may not update it (otherwise it would not be possible to unambiguously +determine process dependencies -- see below). For the same reason, +only one process in a model may compute a value of a variable (i.e., +``intent='out'``). The great advantage of declaring variables at unique places is that all their meta-data are defined once. However, a downside of this @@ -116,10 +126,10 @@ approach is that foreign variables may potentially add many hard-coded links between processes, which makes harder reusing these processes independently of each other. -Variable groups +Group variables ~~~~~~~~~~~~~~~ -In some cases, using variable groups may provide an elegant +In some cases, using group variables may provide an elegant alternative to hard-coded links between processes. The membership of variables to a group is defined via their ``group`` @@ -129,12 +139,16 @@ declare a :func:`~xsimlab.group` variable. The latter behaves like an iterable of foreign variables pointing to each of the variables that are members of the group, across the model. -Variable groups are useful particularly in cases where you want to -combine different processes that act on the same variable, e.g. in -landscape evolution modeling combine the effect of different erosion -processes on the evolution of the surface elevation. This way you can -easily add or remove processes to/from a model and avoid missing or -broken links between processes. +Note that group variables only support ``intent='in'``, i.e, group +variables should only be used to get the values of multiple foreign +variables of a same group. + +Group variables are useful particularly in cases where you want to +combine (aggregate) different processes that act on the same +variable, e.g. in landscape evolution modeling combine the effect of +different erosion processes on the evolution of the surface +elevation. This way you can easily add or remove processes to/from a +model and avoid missing or broken links between processes. On-demand variables ~~~~~~~~~~~~~~~~~~~ @@ -144,9 +158,9 @@ value is not intended to be computed systematically, e.g., at each time step of a simulation, but instead only at a given few times. These are declared using :func:`~xsimlab.on_demand` and must implement in the same process-ified class a dedicated method that -computes their value. +computes their value. They have always ``intent='out'``. -These variables are useful, e.g., for model diagnostics. +On-demand variables are useful, e.g., for model diagnostics. Simulation workflow ------------------- @@ -162,11 +176,13 @@ During a simulation, stages 1 and 4 are run only once while stages 2 and 3 are repeated for a given number of (time) steps. Each process-ified class may provide its own computation instructions -for those stages by implementing specific methods (one per -stage). Note that this is entirely optional. For example, -time-independent processes (e.g., for setting model grids) usually -implement stage 1 only. In a few cases, the role of a process may even -consist of just declaring some variables that are used elsewhere. +by implementing specific methods named ``.initialize()``, +``.run_step()``, ``.finalize_step()`` and ``.finalize()`` for each +stage above, respectively. Note that this is entirely optional. For +example, time-independent processes (e.g., for setting model grids) +usually implement stage 1 only. In a few cases, the role of a process +may even consist of just declaring some variables that are used +elsewhere. Get / set variable values inside a process ------------------------------------------ @@ -182,12 +198,12 @@ takes all variables declared as class attributes and turns them into properties, which may be read-only depending on the ``intent`` set for the variables. -Basically, these properties read/write values from/into a simple -key-value store (except for on-demand variables). Currently the store -is fully in-memory but it could be easily replaced by an on-disk or a -distributed store. The xarray-simlab's modeling framework can thus be -viewed as a thin object-oriented layer built on top of an abstract -key-value store. +Basically, the getter (setter) methods of these properties read +(write) values from (into) a simple key-value store (except for +on-demand variables). Currently the store is fully in-memory but it +could be easily replaced by an on-disk or a distributed store. The +xarray-simlab's modeling framework can thus be viewed as a thin +object-oriented layer built on top of an abstract key-value store. Process dependencies and ordering --------------------------------- diff --git a/doc/inspect_model.rst b/doc/inspect_model.rst index 59599310..c742ebd6 100644 --- a/doc/inspect_model.rst +++ b/doc/inspect_model.rst @@ -13,19 +13,45 @@ this user guide. import sys sys.path.append('scripts') - from advection_model import model2 + from advection_model import model2, ProfileU -Inspect processes and variables -------------------------------- +.. ipython:: python + + import xsimlab as xs + +Inspect model inputs +-------------------- Model *repr* already gives information about the number and names of -processes and their variables that need an input value (if provided, a -short description is also displayed for these variables): +processes and their variables that need an input value (if any): .. ipython:: python model2 +For each input, a one-line summary is shown with the intent (either +'in' or 'inout') as well as the dimension labels for inputs that don't +expect a scalar value only. If provided, a short description is also +displayed in the summary. + +The convenient property :attr:`~xsimlab.Model.input_vars` of Model +returns all inputs as a list of 2-length tuples with process and +variable names, respectively. + +.. ipython:: python + + model2.input_vars + +:attr:`~xsimlab.Model.input_vars_dict` returns all inputs grouped by +process, as a dictionary: + +.. ipython:: python + + model2.input_vars_dict + +Inspect processes and variables +------------------------------- + For deeper inspection, Model objects support both dict-like and attribute-like access to their processes, e.g., @@ -36,34 +62,34 @@ attribute-like access to their processes, e.g., As shown here above, process *repr* includes: -- the path to the corresponding Process subclass (top line) ; +- the name to the process class and the name of the process in the model + (top line) ; - a "Variables" section with all variables declared in the process - (not only model inputs), their types (e.g., ``FloatVariable``, - ``ForeignVariable``) and some additional information depending on - their type like dimension labels for regular variables or references - to original variables for ``ForeignVariable`` objects ; -- a "Meta" section with all process metadata. - -In "Variables" section, symbol ``*`` means that a value for the -variable is provided by the process itself (i.e., ``provided=True``). + (not only model inputs) including one-line summaries that depend on + their type (i.e., ``variable``, ``foreign``, ``group``, etc.) ; +- a "Simulation stages" section with the stages that are implemented + in the process. -Processes also support dict-like and attribute-like access to all -their declared variables, e.g., +It is also possible to inspect a process class taken individually with +:func:`~xsimlab.process_info`: .. ipython:: python - model2['advect']['v'] - model2.grid.x + xs.process_info(ProfileU) -It is also possible to have direct access to all input variables in a -model using the :attr:`~xsimlab.Model.input_vars` property. - -We can further test whether a variable is a model input or not, e.g., +Similarly, :func:`~xsimlab.variable_info` allows inspection at the +variable level: .. ipython:: python - model2.is_input(('advect', 'v')) - model2.is_input(('profile', 'u')) + xs.variable_info(ProfileU, 'u') + xs.variable_info(model2.profile, 'u_vars') + +Like :attr:`~xsimlab.Model.input_vars` and +:attr:`~xsimlab.Model.input_vars_dict`, Model properties +:attr:`~xsimlab.Model.all_vars` and +:attr:`~xsimlab.Model.all_vars_dict` are available for all model +variables, not only inputs. Visualize models as graphs -------------------------- @@ -115,10 +141,10 @@ square nodes: :width: 60% Nodes with solid border correspond to regular variables while nodes -with dashed border correspond to ``ForeignVariable`` objects. 3d-box -nodes correspond to iterables of Variable objects, like -``VariableGroup``. Variables connected to their process with an arrow -have a value provided by the process. +with dashed border correspond to foreign variables. 3d-box nodes +correspond group variables. Variables connected to their process with +an arrow have a value computed by the process itself (i.e., +``intent='out'``). A third option ``show_only_variable`` allows to show only one given variable and all its references in other processes, e.g., diff --git a/doc/run_model.rst b/doc/run_model.rst index 9f6bb3b7..fb5b14ab 100644 --- a/doc/run_model.rst +++ b/doc/run_model.rst @@ -20,7 +20,7 @@ The following imports are necessary for the examples below. .. ipython:: python - import xsimlab + import xsimlab as xs import matplotlib.pyplot as plt .. note:: @@ -45,16 +45,16 @@ create a new setup in a very declarative way: .. ipython:: python - in_ds = xsimlab.create_setup( + in_ds = xs.create_setup( model=model2, clocks={'time': {'start': 0, 'end': 1, 'step': 0.01}, - 'stime': {'data': [0, 0.5, 1]}}, + 'otime': {'data': [0, 0.5, 1]}}, master_clock='time', input_vars={'grid': {'length': 1.5, 'spacing': 0.01}, 'advect': {'v': 1.}, 'init': {'loc': 0.3, 'scale': 0.1}}, - snapshot_vars={None: {'grid': 'x'}, - 'stime': {'profile': 'u'}} + output_vars={None: {'grid': 'x'}, + 'otime': {'profile': 'u'}} ) A setup consists in: @@ -71,7 +71,7 @@ A setup consists in: (``None``). In the example above, we set ``time`` as the master clock dimension -and ``stime`` as another dimension for taking snapshots of :math:`u` +and ``otime`` as another dimension for taking snapshots of :math:`u` along the grid at three given times of the simulation (beginning, middle and end). The time-independent x-coordinate values of the grid will be saved as well. @@ -94,24 +94,21 @@ variables, e.g., Run a simulation ---------------- -A simple call to :meth:`.xsimlab.run` from the input dataset created -above is needed to perform the simulation. It returns a new dataset: +A new simulation is run by simply calling the :meth:`.xsimlab.run` +method from the input dataset created above. It returns a new dataset: .. ipython:: python out_ds = in_ds.xsimlab.run(model=model2) The returned dataset contains all the variables of the input -dataset. It also contains snapshots values as new data variables, -e.g., ``grid__x`` and ``profile__u`` in this example: +dataset. It also contains simulation outputs as new or updated data +variables, e.g., ``grid__x`` and ``profile__u`` in this example: .. ipython:: python out_ds -Note that in other cases snapshots may also result in updated data -variables with an additional time dimension. - Post-processing and plotting ---------------------------- @@ -137,8 +134,8 @@ e.g., for plotting snapshots: def plot_u(ds): fig, axes = plt.subplots(ncols=3, figsize=(10, 3)) - for t, ax in zip(ds.stime, axes): - ds.profile__u.sel(stime=t).plot(ax=ax) + for t, ax in zip(ds.otime, axes): + ds.profile__u.sel(otime=t).plot(ax=ax) fig.tight_layout() return fig @@ -159,7 +156,7 @@ update only the value of velocity, thanks to .. ipython:: python - in_vars = {'advect': {'v': 0.5}} + in_vars = {('advect', 'v'): 0.5} with model2: out_ds2 = (in_ds.xsimlab.update_vars(input_vars=in_vars) .xsimlab.run() @@ -167,7 +164,7 @@ update only the value of velocity, thanks to .. note:: - For convenience, we may use a Model instance in a context instead + For convenience, a Model instance may be used in a context instead of providing it repeatedly as an argument of xarray-simlab's functions or methods in which it is required. @@ -185,13 +182,13 @@ Update time dimensions :meth:`.xsimlab.update_clocks` allows to only update the time dimensions and/or their coordinates. Here below we set other values -for the ``stime`` coordinate (which serves to take snapshots of +for the ``otime`` coordinate (which serves to take snapshots of :math:`u`): .. ipython:: python clocks = {'time': {'data': in_ds.time}, - 'stime': {'data': [0, 0.25, 0.5]}} + 'otime': {'data': [0, 0.25, 0.5]}} with model2: out_ds3 = (in_ds.xsimlab.update_clocks(clocks=clocks, master_clock='time') diff --git a/doc/scripts/advection_model.py b/doc/scripts/advection_model.py index f03d81e2..4547c663 100644 --- a/doc/scripts/advection_model.py +++ b/doc/scripts/advection_model.py @@ -1,158 +1,144 @@ import numpy as np -from xsimlab import Process, FloatVariable +import xsimlab as xs -class AdvectionLax1D(Process): +@xs.process +class AdvectionLax1D(object): """Wrap 1-dimensional advection in a single Process.""" - spacing = FloatVariable((), description='grid spacing') - length = FloatVariable((), description='grid total length') - x = FloatVariable('x', provided=True) + spacing = xs.variable(description='grid spacing') + length = xs.variable(description='grid total length') + x = xs.variable(dims='x', intent='out') - v = FloatVariable([(), 'x'], description='velocity') + v = xs.variable(dims=[(), 'x'], description='velocity') - loc = FloatVariable((), description='location of initial profile') - scale = FloatVariable((), description='scale of initial profile') - u = FloatVariable('x', description='quantity u', - attrs={'units': 'm'}, provided=True) + loc = xs.variable(description='location of initial profile') + scale = xs.variable(description='scale of initial profile') + u = xs.variable(dims='x', intent='out', description='quantity u', + attrs={'units': 'm'}) def initialize(self): - self.x.value = np.arange(0, self.length.value, self.spacing.value) - self.u.state = np.exp(-1 / self.scale.value**2 * - (self.x.value - self.loc.value)**2) + self.x = np.arange(0, self.length, self.spacing) + self.u = np.exp(-1 / self.scale**2 * (self.x - self.loc)**2) def run_step(self, dt): - factor = (self.v.value * dt) / (2 * self.spacing.value) - u_left = np.roll(self.u.state, 1) - u_right = np.roll(self.u.state, -1) + factor = (self.v * dt) / (2 * self.spacing) + u_left = np.roll(self.u, 1) + u_right = np.roll(self.u, -1) self.u1 = 0.5 * (u_right + u_left) - factor * (u_right - u_left) def finalize_step(self): - self.u.state = self.u1 + self.u = self.u1 -from xsimlab import Model +model1 = xs.Model({'advect': AdvectionLax1D}) -model1 = Model({'advect': AdvectionLax1D}) - - -class UniformGrid1D(Process): +@xs.process +class UniformGrid1D(object): """Create a 1-dimensional, equally spaced grid.""" - spacing = FloatVariable((), description='uniform spacing') - length = FloatVariable((), description='total length') - x = FloatVariable('x', provided=True) - - class Meta: - time_dependent = False + spacing = xs.variable(description='uniform spacing') + length = xs.variable(description='total length') + x = xs.variable(dims='x', intent='out') def initialize(self): - self.x.value = np.arange(0, self.length.value, self.spacing.value) - - -from xsimlab import VariableGroup + self.x = np.arange(0, self.length, self.spacing) -class ProfileU(Process): +@xs.process +class ProfileU(object): """Compute the evolution of the profile of quantity `u`.""" - u_vars = VariableGroup('u_vars') - u = FloatVariable('x', description='quantity u', - attrs={'units': 'm'}) + u_vars = xs.group('u_vars') + u = xs.variable(dims='x', intent='inout', description='quantity u', + attrs={'units': 'm'}) def run_step(self, *args): - self.u.change = sum((var.change for var in self.u_vars)) + self._delta_u = sum((v for v in self.u_vars)) def finalize_step(self): - self.u.state += self.u.change + self.u += self._delta_u -from xsimlab import ForeignVariable - - -class AdvectionLax(Process): +@xs.process +class AdvectionLax(object): """Advection using finite difference (Lax method) on a fixed grid with periodic boundary conditions. """ - v = FloatVariable([(), 'x'], description='velocity') - grid_spacing = ForeignVariable(UniformGrid1D, 'spacing') - u = ForeignVariable(ProfileU, 'u') - u_advected = FloatVariable('x', provided=True, group='u_vars') + v = xs.variable(dims=[(), 'x'], description='velocity') + grid_spacing = xs.foreign(UniformGrid1D, 'spacing') + u = xs.foreign(ProfileU, 'u') + u_advected = xs.variable(dims='x', intent='out', group='u_vars') def run_step(self, dt): - factor = self.v.value / (2 * self.grid_spacing.value) + factor = self.v / (2 * self.grid_spacing) - u_left = np.roll(self.u.state, 1) - u_right = np.roll(self.u.state, -1) + u_left = np.roll(self.u, 1) + u_right = np.roll(self.u, -1) u_1 = 0.5 * (u_right + u_left) - factor * dt * (u_right - u_left) - self.u_advected.change = u_1 - self.u.state + self.u_advected = u_1 - self.u -class InitUGauss(Process): - """Initialize `u` profile using a gaussian pulse.""" +@xs.process +class InitUGauss(object): + """Initialize `u` profile using a Gaussian pulse.""" - loc = FloatVariable((), description='location of initial pulse') - scale = FloatVariable((), description='scale of initial pulse') - x = ForeignVariable(UniformGrid1D, 'x') - u = ForeignVariable(ProfileU, 'u', provided=True) - - class Meta: - time_dependent = False + loc = xs.variable(description='location of initial pulse') + scale = xs.variable(description='scale of initial pulse') + x = xs.foreign(UniformGrid1D, 'x') + u = xs.foreign(ProfileU, 'u', intent='out') def initialize(self): - self.u.state = np.exp( - -1 / self.scale.value**2 * (self.x.value - self.loc.value)**2 - ) + self.u = np.exp(-1 / self.scale**2 * (self.x - self.loc)**2) -model2 = Model({'grid': UniformGrid1D, - 'profile': ProfileU, - 'init': InitUGauss, - 'advect': AdvectionLax}) +model2 = xs.Model({'grid': UniformGrid1D, + 'profile': ProfileU, + 'init': InitUGauss, + 'advect': AdvectionLax}) -class SourcePoint(Process): +@xs.process +class SourcePoint(object): """Source point for quantity `u`. The location of the source point is adjusted to coincide with the nearest node the grid. """ - loc = FloatVariable((), description='source location') - flux = FloatVariable((), description='source flux') - x = ForeignVariable(UniformGrid1D, 'x') - u_source = FloatVariable('x', provided=True, group='u_vars') + loc = xs.variable(description='source location') + flux = xs.variable(description='source flux') + x = xs.foreign(UniformGrid1D, 'x') + u_source = xs.variable(dims='x', intent='out', group='u_vars') @property def nearest_node(self): - idx = np.abs(self.x.value - self.loc.value).argmin() + idx = np.abs(self.x - self.loc).argmin() return idx @property def source_rate(self): - src_array = np.zeros_like(self.x.value) - src_array[self.nearest_node] = self.flux.value + src_array = np.zeros_like(self.x) + src_array[self.nearest_node] = self.flux return src_array def run_step(self, dt): - self.u_source.change = self.source_rate * dt + self.u_source = self.source_rate * dt -class InitUFlat(Process): +@xs.process +class InitUFlat(object): """Flat initial profile of `u`.""" - x = ForeignVariable(UniformGrid1D, 'x') - u = ForeignVariable(ProfileU, 'u', provided=True) - - class Meta: - time_dependent = False + x = xs.foreign(UniformGrid1D, 'x') + u = xs.foreign(ProfileU, 'u', intent='out') def initialize(self): - self.u.state = np.zeros_like(self.x.value) + self.u = np.zeros_like(self.x) model3 = model2.update_processes({'source': SourcePoint, diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 2224011d..21718de9 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -116,6 +116,10 @@ Regressions (will be fixed in future releases) - Although it is possible to set validators, converters and/or default values for variables (this is directly supported by ``attrs``), these are not handled by xarray-simlab yet. +- Variables don't accept anymore a dimension that corresponds to their + own name. This may be useful, e.g., for sensitivity analysis, but as + the latter is not implemented yet this feature will be added back in + a next release. v0.1.1 (20 November 2017) ------------------------- From 8a6ae4ebd78c27ce2ac430df94f8e54616153b2b Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 14:21:25 +0200 Subject: [PATCH 90/97] doc add some content --- doc/faq.rst | 9 ++++++--- doc/inspect_model.rst | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/faq.rst b/doc/faq.rst index 51748b8a..386d22f9 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -35,17 +35,20 @@ main bottleneck of the overall model execution, especially when using an implicit time scheme. For inner (e.g., spatial) loops in each model processes, it might be better to have a numpy vectorized implementation, use tools like Cython_ or Numba_ or call wrapped code -that is written in, e.g., C or Fortran (see for example f2py_ for -wrapping Fortran code). +that is written in, e.g., C/C++ or Fortran (see for example f2py_ for +wrapping Fortran code or pybind11_ for wrapping C++11 code). As with any other framework, xarray-simlab introduces an overhead compared to a simple, straightforward (but non-flexible) implementation of a model. The preliminary benchmarks that we have run -show only a very small overhead, though. +show only a very small overhead, though. This overhead is mainly +introduced by the thin object-oriented layer that model components +(i.e., Python classes) together form. .. _Cython: http://cython.org/ .. _Numba: http://numba.pydata.org/ .. _f2py: https://docs.scipy.org/doc/numpy-dev/f2py/ +.. _pybind11: https://pybind11.readthedocs.io Does xarray-simlab support running model(s) in parallel? -------------------------------------------------------- diff --git a/doc/inspect_model.rst b/doc/inspect_model.rst index c742ebd6..231041f3 100644 --- a/doc/inspect_model.rst +++ b/doc/inspect_model.rst @@ -85,6 +85,13 @@ variable level: xs.variable_info(ProfileU, 'u') xs.variable_info(model2.profile, 'u_vars') +Alternatively, we can look at the docstrings of auto-generated +properties for each variable, e.g., + +.. ipython:: python + + ProfileU.u? + Like :attr:`~xsimlab.Model.input_vars` and :attr:`~xsimlab.Model.input_vars_dict`, Model properties :attr:`~xsimlab.Model.all_vars` and From c454c879650e9021b0eefcd0fe41bd50c594adb8 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 14:21:53 +0200 Subject: [PATCH 91/97] try build docs in test matrix (travis) --- .travis.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 39385053..2e592907 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: env: CONDA_ENV=py36-xarray-dev - python: 3.6 env: CONDA_ENV=py36-attrs-dev + - python: 3.5 + env: CONDA_ENV=docs allow_failures: - python: 3.6 env: CONDA_ENV=py36-xarray-dev @@ -36,8 +38,12 @@ before_install: - conda info -a install: - - conda env create --file ci/requirements-$CONDA_ENV.yml - - source activate test_env_$CONDA_ENV + - if [[ "$CONDA_ENV" == "docs" ]]; then + conda env create -n test_env --file doc/environment.yml; + else + conda env create -n test_env --file ci/requirements-$CONDA_ENV.yml; + fi + - source activate test_env - conda list - pip install --no-deps -e . From 86aa886d7090f2d515c8fcbc6d4f4eef616ddfac Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 14:43:26 +0200 Subject: [PATCH 92/97] doc build in travis: build docs instead of running tests --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2e592907..dd2e7078 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,13 @@ install: - pip install --no-deps -e . script: - - py.test xsimlab --cov=xsimlab --cov-report term-missing --verbose + - python -OO -c "import xsimlab" + - if [[ "$CONDA_ENV" == "docs" ]]; then + conda install -c conda-forge sphinx_rtd_theme; + sphinx-build -n -j auto -b html -d _build/doctrees doc _build/html; + else + py.test xsimlab --cov=xsimlab --cov-report term-missing --verbose; + fi after_success: - coveralls From b54b316370ef19d4c2cdaab95ef80c90aee4a9c1 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Sun, 6 May 2018 15:13:43 +0200 Subject: [PATCH 93/97] try fix doc build in travis 2 --- .travis.yml | 3 ++- doc/environment.yml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd2e7078..6af22dc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,8 @@ script: - python -OO -c "import xsimlab" - if [[ "$CONDA_ENV" == "docs" ]]; then conda install -c conda-forge sphinx_rtd_theme; - sphinx-build -n -j auto -b html -d _build/doctrees doc _build/html; + cd doc; + sphinx-build -n -j auto -b html -d _build/doctrees . _build/html; else py.test xsimlab --cov=xsimlab --cov-report term-missing --verbose; fi diff --git a/doc/environment.yml b/doc/environment.yml index 95f183ec..6134daf4 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -8,12 +8,12 @@ dependencies: - numpy=1.12 - pandas=0.22 - xarray=0.10.0 - - ipython=6.2.1 + - ipython=6.3.1 - ipykernel=4.6.1 - matplotlib=2.0.2 - graphviz - python-graphviz - nbconvert=5.2.1 - - sphinx=1.6.2 - - nbsphinx=0.3.1 + - sphinx=1.7.4 + - nbsphinx=0.3.3 - pandoc=1.19.2 From 1510d4f8c7e5ecfe292ff24d1304509edb3a928b Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 7 May 2018 09:57:56 +0200 Subject: [PATCH 94/97] disable push build on PRs (push build only on master) --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6af22dc7..fa54f028 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ language: python sudo: false # use container based build notifications: email: false +branches: + only: + - master matrix: fast_finish: True From 366fc36b96b000dbcefbe67bd960e5624d52cc19 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 7 May 2018 11:40:35 +0200 Subject: [PATCH 95/97] simplify doc conf (rtd theme) --- doc/conf.py | 10 +--------- doc/run_model.rst | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f2a3a47d..637986e0 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -125,15 +125,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # - -# on_rtd is whether we are on readthedocs.org, this line of code grabbed from -# docs.readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = 'sphinx_rtd_theme' # otherwise, readthedocs.org uses their theme by default, so no need # to specify it diff --git a/doc/run_model.rst b/doc/run_model.rst index fb5b14ab..05a3e602 100644 --- a/doc/run_model.rst +++ b/doc/run_model.rst @@ -223,8 +223,8 @@ flat initial profile for :math:`u`) instead of ``model2`` : Time-varying input values ------------------------- -All model inputs accept as values arrays which have a dimension that -corresponds to the master clock. +All model inputs accept arrays which have a dimension that corresponds +to the master clock. The example below is based on the last example above, but instead of being fixed, the flux of :math:`u` at the source point decreases over From f70da011a7215117687d9d1aa4c02b997cb79672 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 7 May 2018 12:02:57 +0200 Subject: [PATCH 96/97] require attrs >= 18.1.0 (use fields_dict internally) --- ci/requirements-py35.yml | 2 +- ci/requirements-py36-xarray-dev.yml | 2 +- ci/requirements-py36.yml | 2 +- doc/develop.rst | 2 +- doc/installing.rst | 9 +++++---- doc/whats_new.rst | 4 ++++ setup.py | 2 +- xsimlab/utils.py | 25 ++++++------------------- 8 files changed, 20 insertions(+), 28 deletions(-) diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index f44638ce..4aa4cb1b 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -2,7 +2,7 @@ name: test_env_py35 channels: - conda-forge dependencies: - - attrs + - attrs>=18.1.0 - python=3.5 - pytest - numpy diff --git a/ci/requirements-py36-xarray-dev.yml b/ci/requirements-py36-xarray-dev.yml index 4e6fb640..b4f1732e 100644 --- a/ci/requirements-py36-xarray-dev.yml +++ b/ci/requirements-py36-xarray-dev.yml @@ -2,7 +2,7 @@ name: test_env_py36-xarray-dev channels: - conda-forge dependencies: - - attrs + - attrs>=18.1.0 - python=3.6 - pytest - numpy diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index 272a85b3..d42b692e 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -2,7 +2,7 @@ name: test_env_py36 channels: - conda-forge dependencies: - - attrs + - attrs>=18.1.0 - python=3.6 - pytest - numpy diff --git a/doc/develop.rst b/doc/develop.rst index 086ffed4..b0eba214 100644 --- a/doc/develop.rst +++ b/doc/develop.rst @@ -53,7 +53,7 @@ To install the dependencies, we recommend using the conda_ package manager with the conda-forge_ channel. For development purpose, you might consider installing the packages in a new conda environment:: - $ conda create -n xarray-simlab_dev python=3.6 numpy xarray -c conda-forge + $ conda create -n xarray-simlab_dev python=3.6 attrs numpy xarray -c conda-forge $ source activate xarray-simlab_dev Then install xarray-simlab locally using ``pip``:: diff --git a/doc/installing.rst b/doc/installing.rst index ea6f8658..9be8e8f2 100644 --- a/doc/installing.rst +++ b/doc/installing.rst @@ -6,9 +6,10 @@ Install xarray-simlab Required dependencies --------------------- -- Python 3.4 or later. +- Python 3.5 or later. +- `attrs `__ (18.1.0 or later) - `numpy `__ -- `xarray `__ (0.8.0 or later) +- `xarray `__ (0.10.0 or later) Optional dependencies --------------------- @@ -49,12 +50,12 @@ To install xarray-simlab from source, be sure you have the required dependencies (numpy and xarray) installed first. You might consider using conda_ to install them:: - $ conda install xarray numpy pip -c conda-forge + $ conda install attrs xarray numpy pip -c conda-forge A good practice (especially for development purpose) is to install the packages in a separate environment, e.g. using conda:: - $ conda create -n simlab_py36 python=3.6 xarray numpy pip -c conda-forge + $ conda create -n simlab_py36 python=3.6 attrs xarray numpy pip -c conda-forge $ source activate simlab_py36 Then you can clone the xarray-simlab git repository and install it diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 21718de9..7a929eee 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -14,6 +14,10 @@ the API on how processes and variables are defined and depends on each other in a model. xarray-simlab now uses and extends attrs_ (:issue:`33`). +Also, Python 3.4 support has been dropped. It may still work with that +version but it is not actively tested anymore and it is not packaged +with conda. + .. _attrs: http://www.attrs.org Breaking changes diff --git a/setup.py b/setup.py index 9137bd50..4095835d 100755 --- a/setup.py +++ b/setup.py @@ -20,6 +20,6 @@ long_description=(open('README.rst').read() if exists('README.rst') else ''), python_requires='>=3.5', - install_requires=['attrs', 'numpy', 'xarray >= 0.10.0'], + install_requires=['attrs >= 18.1.0', 'numpy', 'xarray >= 0.10.0'], tests_require=['pytest >= 3.3.0'], zip_safe=False) diff --git a/xsimlab/utils.py b/xsimlab/utils.py index 566b1ab1..67dab1e5 100644 --- a/xsimlab/utils.py +++ b/xsimlab/utils.py @@ -7,31 +7,18 @@ OrderedDict) from contextlib import suppress from importlib import import_module -from inspect import isclass -import attr - - -def attr_fields_dict(cls): - # TODO: remove this and use attr.fields_dict instead (18.1.0) - if not isclass(cls): - raise TypeError("Passed object must be a class.") - attrs = getattr(cls, "__attrs_attrs__", None) - if attrs is None: - raise attr.NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return OrderedDict(((a.name, a) for a in attrs)) +from attr import fields_dict def variables_dict(process_cls): - """Get all xsimlab variables declared in a process.""" + """Get all xsimlab variables declared in a process. - # exclude attr.Attribute objects that are not xsimlab-specific - vars = OrderedDict((k, v) - for k, v in attr_fields_dict(process_cls).items() + Exclude attr.Attribute objects that are not xsimlab-specific. + """ + return OrderedDict((k, v) + for k, v in fields_dict(process_cls).items() if 'var_type' in v.metadata) - return vars def has_method(obj, meth): From 6f0f8bea5e7c620bdb3e1fb973dc1f15f8d139ee Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Mon, 7 May 2018 15:38:22 +0200 Subject: [PATCH 97/97] doc update examples --- doc/examples/landscape-evolution-model.ipynb | 2146 +++++++++--------- doc/faq.rst | 6 +- doc/whats_new.rst | 4 +- 3 files changed, 1031 insertions(+), 1125 deletions(-) diff --git a/doc/examples/landscape-evolution-model.ipynb b/doc/examples/landscape-evolution-model.ipynb index fd85dacd..b551e6c2 100644 --- a/doc/examples/landscape-evolution-model.ipynb +++ b/doc/examples/landscape-evolution-model.ipynb @@ -19,7 +19,7 @@ "source": [ "import numpy as np\n", "import xarray as xr\n", - "import xsimlab" + "import xsimlab as xs" ] }, { @@ -28,7 +28,7 @@ "source": [ "## Import and inspect a model\n", "\n", - "The model (i.e., the `xsimlab.Model` object) that we use here is provided by the [xarray-topo](https://gitext.gfz-potsdam.de/sec55-public/xarray-topo) package." + "The model (i.e., the `xsimlab.Model` object) that we use here is provided by the [xarray-topo](https://gitext.gfz-potsdam.de/sec55-public/xarray-topo) package (**Note:** check the version of this package below, it may not correspond to the latest stable release)." ] }, { @@ -40,7 +40,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "v0.0.10\n" + "v0.0.10+0.gb27cf6e.dirty\n" ] } ], @@ -93,32 +93,28 @@ { "data": { "text/plain": [ - "\n", + "\n", "grid\n", - " x_length (in) total grid length in x\n", - " x_origin (in) grid x-origin\n", - " x_size (in) nb. of nodes in x\n", - " x_spacing (in) node spacing in x\n", - " y_length (in) total grid length in y\n", - " y_origin (in) grid y-origin\n", - " y_size (in) nb. of nodes in y\n", - " y_spacing (in) node spacing in y\n", + " y_size [in] nb. of nodes in y\n", + " y_length [in] total grid length in y\n", + " x_size [in] nb. of nodes in x\n", + " x_length [in] total grid length in x\n", "boundaries\n", "block_uplift\n", - " u_coef (in) uplift rate\n", + " u_coef [in] () or ('y', 'x') uplift rate\n", "flow_routing\n", - " pit_method (in) \n", + " pit_method [in]\n", "area\n", "spower\n", - " k_coef (in) stream-power constant\n", - " m_exp (in) stream-power drainage area exponent\n", - " n_exp (in) stream-power slope exponent\n", + " n_exp [in] stream-power slope exponent\n", + " k_coef [in] stream-power constant\n", + " m_exp [in] stream-power drainage area exponent\n", "diffusion\n", - " k_coef (in) diffusivity\n", + " k_coef [in] diffusivity\n", "erosion\n", "uplift\n", "topography\n", - " elevation (in) topographic elevation" + " elevation [inout] ('y', 'x') topographic elevation" ] }, "execution_count": 4, @@ -146,7 +142,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABKkAAAIzCAYAAADLbkqhAAAAAXNSR0IArs4c6QAAQABJREFUeAHs\n3Qd8FVXaBvAnvffeSUgooffeQRRUwN4F1q5r2VVX1/1W3bWXtTdUsGIBUVBEpPdeAoSEFNJDeu/1\ne8+EGxJKSCAh9948x98w986cmTnzn4C57z3nPSb19fU/goUCFKAABShAAQpQgAIUoAAFKEABClCA\nAp0nUG0iQar6zrs+r0wBClCAAhSgAAUoQAEKUIACFKAABShAAVSaEoECFKAABShAAQpQgAIUoAAF\nKEABClCAAp0twCBVZz8BXp8CFKAABShAAQpQgAIUoAAFKEABClAADFLxh4ACFKAABShAAQpQgAIU\noAAFKEABClCg0wXMW9OCqvJ8rP/qatTWVLSmOuu0UcDDfxiGz/qwjUexOgUoQAEKUIACFKAABShA\nAQpQgAIUMB6BVgWpaqpKtABVSPhUWNu5GM/d68GdZKdForw0Ww9awiZQgAIUoAAFKEABClCAAhSg\nAAUoQIHOE2hVkErXPFevUNg5eunect0OAqVFWSgvT2qHM/EUFKAABShAAQpQgAIUoAAFKEABClDA\ncAWYk8pwnx1bTgEKUIACFKAABShAAQpQgAIUoAAFjEaAQSqjeZS8EQpQgAIUoAAFKEABClCAAhSg\nAAUoYLgCDFIZ7rNjyylAAQpQgAIUoAAFKEABClCAAhSggNEIMEhlNI+SN0IBClCAAhSgAAUoQAEK\nUIACFKAABQxXgEEqw312bDkFKEABClCAAhSgAAUoQAEKUIACFDAaAQapjOZR8kYoQAEKUIACFKAA\nBShAAQpQgAIUoIDhCjBIZbjPji2nAAUoQAEKUIACFKAABShAAQpQgAJGI8AgldE8St4IBShAAQpQ\ngAIUoAAFKEABClCAAhQwXAEGqQz32bHlFKAABShAAQpQgAIUoAAFKEABClDAaAQYpDKaR8kboQAF\nKEABClCAAhSgAAUoQAEKUIAChivAIJXhPju2nAIUoAAFKEABClCAAhSgAAUoQAEKGI0Ag1RG8yh5\nIxSgAAUoQAEKUIACFKAABShAAQpQwHAFGKQy3GfHllOAAhSgAAUoQAEKUIACFKAABShAAaMRYJDK\naB4lb4QCFKAABShAAQpQgAIUoAAFKEABChiuAINUhvvs2HIKUIACFKAABShAAQpQgAIUoAAFKGA0\n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\n6GaGr38VpSV5GDDl/1rRrDlVEiABEiABEiABEiABEiABEiABEiABEjg3AYpU52ZkES1Sjq1FbPhP\nGDD5X7Cxd7eIOXESJEACJEACJEACJEACJEACJEACJEACJGAuAhSpzEWyCfspLszE3lXPIqDXLHh3\nm9SEI7FrEiABEiABEiABEiABEiABEiABEiABEmgZAhSpWoZ7g0bd+8eT6GRli74T/tag89iYBEiA\nBEiABEiABEiABEiABEiABEiABFoLAbNk92stF9sa53ls79dIid2E0Ze/Bytrx9Z4CZwzCZAACZAA\nCZAACZAACZAACZAACZAACZyTAC2pzomo5Rpkpx7GoY1vosfwO+DmM6DlJsKRSYAESIAESIAESIAE\nSIAESIAESIAESKCJCVCkamLAje2+rLQQu5b9C519+iN02M2N7YbnkQAJkAAJkAAJkAAJkAAJkAAJ\nkAAJkECrIEB3Pwu9TQfWvoTigkyMumyBzLCDhc6S0yIBEiABEiABEiABEiABEiABEiABEiAB8xCg\nJZV5OJq1l4SIJYg7+DMGTv03bB09zdo3OyMBEiABEiABEiABEiABEiABEiABEiABSyRAkcrC7kp2\nagT2rX4e3QdfD6+u4y1sdpwOCZAACZAACZAACZAACZAACZAACZAACTQNAYpUTcO1Ub2WFGVhx9J/\nwM13AHqNvq9RffAkEiABEiABEiABEiABEiABEiABEiABEmiNBChSWchdO1leKgLVP3HyZDmGTH9O\nwlDx1ljIreE0SIAESIAESIAESIAESIAESIAESIAEmoEAA6c3A+T6DLHnjyeRfSICo2e/D2s71/qc\nwjYkQAIkQAIkQAIkQAIkQAIkQAIkQAIk0GYIUKSygFsZsXk+jketwPBZr8O5S6gFzIhTIAESIAES\nIAESIAESIAESIAESIAESIIHmJUCfsublXWu06N1fInLHJxgw+XF4BIyodZwVJEACJEACJEACJEAC\nJEACJEACJEACJNAeCFCkasG7HLP/e4RvmIe+4x+Cf8+LWnAmHJoESIAESIAESIAESIAESIDMsMMn\nAAAP3klEQVQESIAESIAEWpYARaoW4h938GccWPuikcWv64CrW2gWHJYESIAESIAESIAESIAESIAE\nSIAESIAELIMAY1K1wH2IPbAQ+9c8j7Bht6H7kBtbYAYckgRIgARIgARIgARIgARIgARIgARIgAQs\niwBFqma+HxqDSl38eo68G6HDbmnm0TkcCZAACZAACZAACZAACZAACZAACZAACVgmAYpUzXhfVKA6\nuPF1iUH1MLoOmNuMI3MoEiABEiABEiABEiABEiABEiABEiABErBsAh1OSqk6xcK8FKz6fDbKy4qq\nVnO7mQm4+w7C6NnvN/OoHI4ESIAESIAESIAESIAESIAESIAESIAEWoRAUS1LqpKiHEOgCu0/EzZ2\nzi0yq/Y+aOrxQ8jJTmnvGHj9JEACJEACJEACJEACJEACJEACJEAC7YhALZHKdO1uXt3h4ORh2uW6\nGQkU5GeISJXWjCNyKBIgARIgARIgARIgARIgARIgARIgARJoWQIdW3Z4jk4CJEACJEACJEACJEAC\nJEACJEACJEACJEACAEUqfgpIgARIgARIgARIgARIgARIgARIgARIgARanABFqha/BZwACZAACZAA\nCZAACZAACZAACZAACZAACZAARSp+BkiABEiABEiABEiABEiABEiABEiABEiABFqcAEWqFr8FnAAJ\nkAAJkAAJkAAJkAAJkAAJkAAJkAAJkABFKn4GSIAESIAESIAESIAESIAESIAESIAESIAEWpwARaoW\nvwWcAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAEUqfgZIgARIgARIgARIgARIgARIgARIgARIgARa\nnABFqha/BZwACZAACZAACZAACZAACZAACZAACZAACZAARSp+BkiABEiABEiABEiABEiABEiABEiA\nBEiABFqcAEWqFr8FnAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkABFKn4GSIAESIAESIAESIAESIAE\nSIAESIAESIAEWpwARaoWvwWcAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAEUqfgZIgARIgARIgARI\ngARIgARIgARIgARIgARanABFqha/BZwACZAACZAACZAACZAACZAACZAACZAACZAARao6PgNff7cY\nP/y0vI4jrCIBEiABEiABEiABEiABEiABEiABEiABEmgKAhSp6qD6y9I1+G3ZujqOsIoESIAESIAE\nSIAESIAESIAESIAESIAESKApCFg1Raetvc8PFjyNjh06tPbL4PxJgARIgARIgARIgARIgARIgARI\ngARIoNUQoEhVx62yt7Oto5ZVJEACJEACJEACJEACJEACJEACJEACJEACTUXgvEWq7Tv3IzklzZif\n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\n", + "image/png": "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\n", "text/plain": [ "" ] @@ -175,20 +171,18 @@ { "data": { "text/plain": [ - "\n", + "\n", "Variables:\n", - " * x Variable ('x')\n", - " x_length FloatVariable ()\n", - " x_origin FloatVariable ()\n", - " x_size IntegerVariable ()\n", - " x_spacing FloatVariable ()\n", - " * y Variable ('y')\n", - " y_length FloatVariable ()\n", - " y_origin FloatVariable ()\n", - " y_size IntegerVariable ()\n", - " y_spacing FloatVariable ()\n", - "Meta:\n", - " time_dependent: False" + " x_size [in] nb. of nodes in x\n", + " y_size [in] nb. of nodes in y\n", + " x_length [in] total grid length in x\n", + " y_length [in] total grid length in y\n", + " x_spacing [out]\n", + " y_spacing [out]\n", + " x [out] ('x',) \n", + " y [out] ('y',) \n", + "Simulation stages:\n", + " initialize" ] }, "execution_count": 6, @@ -220,7 +214,7 @@ "nx = 101\n", "ny = 101\n", "\n", - "in_ds = xsimlab.create_setup(\n", + "in_ds = xs.create_setup(\n", " model=fastscape_base_model,\n", " clocks={\n", " 'time': {'end': 1e6, 'step': 1e4},\n", @@ -235,7 +229,7 @@ " 'diffusion': {'k_coef': 1.},\n", " 'block_uplift': {'u_coef': 2e-3}\n", " },\n", - " snapshot_vars={\n", + " output_vars={\n", " 'out': {'topography': 'elevation'},\n", " None: {'grid': ('x', 'y')}\n", " }\n", @@ -287,15 +281,11 @@ " * out (out) float64 0.0 1e+05 2e+05 3e+05 4e+05 ...\n", "Dimensions without coordinates: x, y\n", "Data variables:\n", - " grid__x_length float64 1e+05\n", - " grid__x_origin float64 0.0\n", " grid__x_size int64 101\n", - " grid__x_spacing float64 1e+03\n", - " grid__y_length float64 1e+05\n", - " grid__y_origin float64 0.0\n", " grid__y_size int64 101\n", - " grid__y_spacing float64 1e+03\n", - " topography__elevation (y, x) float64 0.2121 0.1451 0.7867 0.614 ...\n", + " grid__x_length float64 1e+05\n", + " grid__y_length float64 1e+05\n", + " topography__elevation (y, x) float64 0.7405 0.8413 0.5533 0.06639 ...\n", " flow_routing__pit_method " + "" ] }, "metadata": {}, @@ -509,12 +491,244 @@ "data": { "text/html": [ "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "\n", - "\n", "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " if ((id === undefined) || !(id in HoloViews.plot_index)) { return; }\n", + " var comm = window.HoloViews.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete HoloViews.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " window.Bokeh.index[id].model.document.clear();\n", + " delete Bokeh.index[id];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete HoloViews.comms[\"hv-extension-comm\"];\n", + " window.HoloViews.plot_index = {}\n", + "}\n", "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", "\n", - " \n", - " \n", - "\n", - "\n", - "
\n" + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\t\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n", + "\n", + "// Define MPL specific subclasses\n", + "function MPLSelectionWidget() {\n", + " SelectionWidget.apply(this, arguments);\n", + "}\n", + "\n", + "function MPLScrubberWidget() {\n", + " ScrubberWidget.apply(this, arguments);\n", + "}\n", + "\n", + "// Let them inherit from the baseclasses\n", + "MPLSelectionWidget.prototype = Object.create(SelectionWidget.prototype);\n", + "MPLScrubberWidget.prototype = Object.create(ScrubberWidget.prototype);\n", + "\n", + "// Define methods to override on widgets\n", + "var MPLMethods = {\n", + " init_slider : function(init_val){\n", + " if(this.load_json) {\n", + " this.from_json()\n", + " } else {\n", + " this.update_cache();\n", + " }\n", + " if (this.dynamic | !this.cached | (this.current_vals === undefined)) {\n", + " this.update(0)\n", + " } else {\n", + " this.set_frame(this.current_vals[0], 0)\n", + " }\n", + " },\n", + " process_msg : function(msg) {\n", + " var data = msg.content.data;\n", + " this.frames[this.current] = data;\n", + " this.update_cache(true);\n", + " this.update(this.current);\n", + " }\n", + "}\n", + "// Extend MPL widgets with backend specific methods\n", + "extend(MPLSelectionWidget.prototype, MPLMethods);\n", + "extend(MPLScrubberWidget.prototype, MPLMethods);\n", + "\n", + "window.HoloViews.MPLSelectionWidget = MPLSelectionWidget\n", + "window.HoloViews.MPLScrubberWidget = MPLScrubberWidget\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in HoloViews.kernels) && (HoloViews.kernels[plot_id])) {\n", + " HoloViews.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.HoloViews.comms) {\n", + " return HoloViews.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in HoloViews.kernels) && (HoloViews.kernels[plot_id])) {\n", + " var comm = HoloViews.kernels[plot_id].connectToComm(comm_id);\n", + " comm.open();\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " }\n", + " HoloViews.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + "\n", + " window.HoloViews.comm_manager = new JupyterCommManager();\n", + " " ], - "text/plain": [ - "" - ] + "application/vnd.holoviews_load.v0+json": "function HoloViewsWidget() {\n}\n\nHoloViewsWidget.prototype.init_slider = function(init_val){\n if(this.load_json) {\n this.from_json()\n } else {\n this.update_cache();\n }\n}\n\nHoloViewsWidget.prototype.populate_cache = function(idx){\n this.cache[idx].innerHTML = this.frames[idx];\n if (this.embed) {\n delete this.frames[idx];\n }\n}\n\nHoloViewsWidget.prototype.process_error = function(msg){\n}\n\nHoloViewsWidget.prototype.from_json = function() {\n var data_url = this.json_path + this.id + '.json';\n $.getJSON(data_url, $.proxy(function(json_data) {\n this.frames = json_data;\n this.update_cache();\n this.update(0);\n }, this));\n}\n\nHoloViewsWidget.prototype.dynamic_update = function(current){\n if (current === undefined) {\n return\n }\n this.current = current;\n if (this.comm) {\n var msg = {comm_id: this.id+'_client', content: current}\n this.comm.send(msg);\n }\n}\n\nHoloViewsWidget.prototype.update_cache = function(force){\n var frame_len = Object.keys(this.frames).length;\n for (var i=0; i 0) {\n that.time = Date.now();\n that.dynamic_update(that.queue[that.queue.length-1]);\n that.queue = [];\n } else {\n that.wait = false;\n }\n if ((msg.msg_type == \"Ready\") && msg.content) {\n console.log(\"Python callback returned following output:\", msg.content);\n } else if (msg.msg_type == \"Error\") {\n console.log(\"Python failed with the following traceback:\", msg['traceback'])\n }\n }\n var comm = HoloViews.comm_manager.get_client_comm(this.plot_id, this.id+'_client', ack_callback);\n return comm\n }\n}\n\nHoloViewsWidget.prototype.process_msg = function(msg) {\n}\n\nfunction SelectionWidget(frames, id, slider_ids, keyMap, dim_vals, notFound, load_json, mode, cached, json_path, dynamic, plot_id){\n this.frames = frames;\n this.id = id;\n this.plot_id = plot_id;\n this.slider_ids = slider_ids;\n this.keyMap = keyMap\n this.current_frame = 0;\n this.current_vals = dim_vals;\n this.load_json = load_json;\n this.mode = mode;\n this.notFound = notFound;\n this.cached = cached;\n this.dynamic = dynamic;\n this.cache = {};\n this.json_path = json_path;\n this.init_slider(this.current_vals[0]);\n this.queue = [];\n this.wait = false;\n if (!this.cached || this.dynamic) {\n this.comm = this.init_comms();\n }\n}\n\nSelectionWidget.prototype = new HoloViewsWidget;\n\n\nSelectionWidget.prototype.get_key = function(current_vals) {\n var key = \"(\";\n for (var i=0; i Date.now()))) {\n this.queue.push(key);\n return\n }\n this.queue = [];\n this.time = Date.now();\n this.current_frame = key;\n this.wait = true;\n this.dynamic_update(key)\n } else if (key !== undefined) {\n this.update(key)\n }\n}\n\n\n/* Define the ScrubberWidget class */\nfunction ScrubberWidget(frames, num_frames, id, interval, load_json, mode, cached, json_path, dynamic, plot_id){\n this.slider_id = \"_anim_slider\" + id;\n this.loop_select_id = \"_anim_loop_select\" + id;\n this.id = id;\n this.plot_id = plot_id;\n this.interval = interval;\n this.current_frame = 0;\n this.direction = 0;\n this.dynamic = dynamic;\n this.timer = null;\n this.load_json = load_json;\n this.mode = mode;\n this.cached = cached;\n this.frames = frames;\n this.cache = {};\n this.length = num_frames;\n this.json_path = json_path;\n document.getElementById(this.slider_id).max = this.length - 1;\n this.init_slider(0);\n this.wait = false;\n this.queue = [];\n if (!this.cached || this.dynamic) {\n this.comm = this.init_comms()\n }\n}\n\nScrubberWidget.prototype = new HoloViewsWidget;\n\nScrubberWidget.prototype.set_frame = function(frame){\n this.current_frame = frame;\n var widget = document.getElementById(this.slider_id);\n if (widget === null) {\n this.pause_animation();\n return\n }\n widget.value = this.current_frame;\n if(this.cached) {\n this.update(frame)\n } else {\n this.dynamic_update(frame)\n }\n}\n\n\nScrubberWidget.prototype.get_loop_state = function(){\n var button_group = document[this.loop_select_id].state;\n for (var i = 0; i < button_group.length; i++) {\n var button = button_group[i];\n if (button.checked) {\n return button.value;\n }\n }\n return undefined;\n}\n\n\nScrubberWidget.prototype.next_frame = function() {\n this.set_frame(Math.min(this.length - 1, this.current_frame + 1));\n}\n\nScrubberWidget.prototype.previous_frame = function() {\n this.set_frame(Math.max(0, this.current_frame - 1));\n}\n\nScrubberWidget.prototype.first_frame = function() {\n this.set_frame(0);\n}\n\nScrubberWidget.prototype.last_frame = function() {\n this.set_frame(this.length - 1);\n}\n\nScrubberWidget.prototype.slower = function() {\n this.interval /= 0.7;\n if(this.direction > 0){this.play_animation();}\n else if(this.direction < 0){this.reverse_animation();}\n}\n\nScrubberWidget.prototype.faster = function() {\n this.interval *= 0.7;\n if(this.direction > 0){this.play_animation();}\n else if(this.direction < 0){this.reverse_animation();}\n}\n\nScrubberWidget.prototype.anim_step_forward = function() {\n if(this.current_frame < this.length - 1){\n this.next_frame();\n }else{\n var loop_state = this.get_loop_state();\n if(loop_state == \"loop\"){\n this.first_frame();\n }else if(loop_state == \"reflect\"){\n this.last_frame();\n this.reverse_animation();\n }else{\n this.pause_animation();\n this.last_frame();\n }\n }\n}\n\nScrubberWidget.prototype.anim_step_reverse = function() {\n if(this.current_frame > 0){\n this.previous_frame();\n } else {\n var loop_state = this.get_loop_state();\n if(loop_state == \"loop\"){\n this.last_frame();\n }else if(loop_state == \"reflect\"){\n this.first_frame();\n this.play_animation();\n }else{\n this.pause_animation();\n this.first_frame();\n }\n }\n}\n\nScrubberWidget.prototype.pause_animation = function() {\n this.direction = 0;\n if (this.timer){\n clearInterval(this.timer);\n this.timer = null;\n }\n}\n\nScrubberWidget.prototype.play_animation = function() {\n this.pause_animation();\n this.direction = 1;\n var t = this;\n if (!this.timer) this.timer = setInterval(function(){t.anim_step_forward();}, this.interval);\n}\n\nScrubberWidget.prototype.reverse_animation = function() {\n this.pause_animation();\n this.direction = -1;\n var t = this;\n if (!this.timer) this.timer = setInterval(function(){t.anim_step_reverse();}, this.interval);\n}\n\nfunction extend(destination, source) {\n for (var k in source) {\n if (source.hasOwnProperty(k)) {\n destination[k] = source[k];\n }\n }\n return destination;\n}\n\nfunction update_widget(widget, values) {\n if (widget.hasClass(\"ui-slider\")) {\n widget.slider('option', {\n min: 0,\n max: values.length-1,\n dim_vals: values,\n value: 0,\n dim_labels: values\n })\n widget.slider('option', 'slide').call(widget, event, {value: 0})\n } else {\n widget.empty();\n for (var i=0; i\", {\n value: i,\n text: values[i]\n }))\n };\n widget.data('values', values);\n widget.data('value', 0);\n widget.trigger(\"change\");\n };\n}\n\nfunction init_slider(id, plot_id, dim, values, next_vals, labels, dynamic, step, value, next_dim,\n dim_idx, delay, jQueryUI_CDN, UNDERSCORE_CDN) {\n // Slider JS Block START\n function loadcssfile(filename){\n var fileref=document.createElement(\"link\")\n fileref.setAttribute(\"rel\", \"stylesheet\")\n fileref.setAttribute(\"type\", \"text/css\")\n fileref.setAttribute(\"href\", filename)\n document.getElementsByTagName(\"head\")[0].appendChild(fileref)\n }\n loadcssfile(\"https://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css\");\n /* Check if jQuery and jQueryUI have been loaded\n otherwise load with require.js */\n var jQuery = window.jQuery,\n // check for old versions of jQuery\n oldjQuery = jQuery && !!jQuery.fn.jquery.match(/^1\\.[0-4](\\.|$)/),\n jquery_path = '',\n paths = {},\n noConflict;\n var jQueryUI = jQuery.ui;\n // check for jQuery\n if (!jQuery || oldjQuery) {\n // load if it's not available or doesn't meet min standards\n paths.jQuery = jQuery;\n noConflict = !!oldjQuery;\n } else {\n // register the current jQuery\n define('jquery', [], function() { return jQuery; });\n }\n if (!jQueryUI) {\n paths.jQueryUI = jQueryUI_CDN.slice(null, -3);\n } else {\n define('jQueryUI', [], function() { return jQuery.ui; });\n }\n paths.underscore = UNDERSCORE_CDN.slice(null, -3);\n var jquery_require = {\n paths: paths,\n shim: {\n \"jQueryUI\": {\n exports:\"$\",\n deps: ['jquery']\n },\n \"underscore\": {\n exports: '_'\n }\n }\n }\n require.config(jquery_require);\n require([\"jQueryUI\", \"underscore\"], function(jUI, _){\n if (noConflict) $.noConflict(true);\n var vals = values;\n if (dynamic && vals.constructor === Array) {\n var default_value = parseFloat(value);\n var min = parseFloat(vals[0]);\n var max = parseFloat(vals[vals.length-1]);\n var wstep = step;\n var wlabels = [default_value];\n var init_label = default_value;\n } else {\n var min = 0;\n if (dynamic) {\n var max = Object.keys(vals).length - 1;\n var init_label = labels[value];\n var default_value = values[value];\n } else {\n var max = vals.length - 1;\n var init_label = labels[value];\n var default_value = value;\n }\n var wstep = 1;\n var wlabels = labels;\n }\n function adjustFontSize(text) {\n var width_ratio = (text.parent().width()/8)/text.val().length;\n var size = Math.min(0.9, Math.max(0.6, width_ratio))+'em';\n text.css('font-size', size);\n }\n var slider = $('#_anim_widget'+id+'_'+dim);\n slider.slider({\n animate: \"fast\",\n min: min,\n max: max,\n step: wstep,\n value: default_value,\n dim_vals: vals,\n dim_labels: wlabels,\n next_vals: next_vals,\n slide: function(event, ui) {\n var vals = slider.slider(\"option\", \"dim_vals\");\n var next_vals = slider.slider(\"option\", \"next_vals\");\n var dlabels = slider.slider(\"option\", \"dim_labels\");\n if (dynamic) {\n var dim_val = ui.value;\n if (vals.constructor === Array) {\n var label = ui.value;\n } else {\n var label = dlabels[ui.value];\n }\n } else {\n var dim_val = vals[ui.value];\n var label = dlabels[ui.value];\n }\n var text = $('#textInput'+id+'_'+dim);\n text.val(label);\n adjustFontSize(text);\n HoloViews.index[plot_id].set_frame(dim_val, dim_idx);\n if (Object.keys(next_vals).length > 0) {\n var new_vals = next_vals[dim_val];\n var next_widget = $('#_anim_widget'+id+'_'+next_dim);\n update_widget(next_widget, new_vals);\n }\n }\n });\n slider.keypress(function(event) {\n if (event.which == 80 || event.which == 112) {\n var start = slider.slider(\"option\", \"value\");\n var stop = slider.slider(\"option\", \"max\");\n for (var i=start; i<=stop; i++) {\n var delay = i*delay;\n $.proxy(function doSetTimeout(i) { setTimeout($.proxy(function() {\n var val = {value:i};\n slider.slider('value',i);\n slider.slider(\"option\", \"slide\")(null, val);\n }, slider), delay);}, slider)(i);\n }\n }\n if (event.which == 82 || event.which == 114) {\n var start = slider.slider(\"option\", \"value\");\n var stop = slider.slider(\"option\", \"min\");\n var count = 0;\n for (var i=start; i>=stop; i--) {\n var delay = count*delay;\n count = count + 1;\n $.proxy(function doSetTimeout(i) { setTimeout($.proxy(function() {\n var val = {value:i};\n slider.slider('value',i);\n slider.slider(\"option\", \"slide\")(null, val);\n }, slider), delay);}, slider)(i);\n }\n }\n });\n var textInput = $('#textInput'+id+'_'+dim)\n textInput.val(init_label);\n adjustFontSize(textInput);\n });\n}\n\nfunction init_dropdown(id, plot_id, dim, vals, value, next_vals, labels, next_dim, dim_idx, dynamic) {\n var widget = $(\"#_anim_widget\"+id+'_'+dim);\n widget.data('values', vals)\n for (var i=0; i\", {\n value: val,\n text: labels[i]\n }));\n };\n widget.data(\"next_vals\", next_vals);\n widget.val(value);\n widget.on('change', function(event, ui) {\n if (dynamic) {\n var dim_val = parseInt(this.value);\n } else {\n var dim_val = $.data(this, 'values')[this.value];\n }\n var next_vals = $.data(this, \"next_vals\");\n if (Object.keys(next_vals).length > 0) {\n var new_vals = next_vals[dim_val];\n var next_widget = $('#_anim_widget'+id+'_'+next_dim);\n update_widget(next_widget, new_vals);\n }\n var widgets = HoloViews.index[plot_id]\n if (widgets) {\n widgets.set_frame(dim_val, dim_idx);\n }\n });\n}\n\nif (window.HoloViews === undefined) {\n window.HoloViews = {}\n}\n\nvar _namespace = {\n init_slider: init_slider,\n init_dropdown: init_dropdown,\n comms: {},\n comm_status: {},\n index: {},\n plot_index: {},\n kernels: {},\n receivers: {}\n}\n\nfor (var k in _namespace) {\n if (!(k in window.HoloViews)) {\n window.HoloViews[k] = _namespace[k];\n }\n}\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if (!output.data.hasOwnProperty(EXEC_MIME_TYPE)) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n toinsert[0].children[0].innerHTML = output.data[HTML_MIME_TYPE];\n toinsert[0].children[1].textContent = output.data[JS_MIME_TYPE];\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n HoloViews.plot_index[id] = Bokeh.index[id];\n } else {\n HoloViews.plot_index[id] = null;\n }\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n if ((id === undefined) || !(id in HoloViews.plot_index)) { return; }\n var comm = window.HoloViews.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete HoloViews.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n window.Bokeh.index[id].model.document.clear();\n delete Bokeh.index[id];\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete HoloViews.comms[\"hv-extension-comm\"];\n window.HoloViews.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\t\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n\n// Define MPL specific subclasses\nfunction MPLSelectionWidget() {\n SelectionWidget.apply(this, arguments);\n}\n\nfunction MPLScrubberWidget() {\n ScrubberWidget.apply(this, arguments);\n}\n\n// Let them inherit from the baseclasses\nMPLSelectionWidget.prototype = Object.create(SelectionWidget.prototype);\nMPLScrubberWidget.prototype = Object.create(ScrubberWidget.prototype);\n\n// Define methods to override on widgets\nvar MPLMethods = {\n init_slider : function(init_val){\n if(this.load_json) {\n this.from_json()\n } else {\n this.update_cache();\n }\n if (this.dynamic | !this.cached | (this.current_vals === undefined)) {\n this.update(0)\n } else {\n this.set_frame(this.current_vals[0], 0)\n }\n },\n process_msg : function(msg) {\n var data = msg.content.data;\n this.frames[this.current] = data;\n this.update_cache(true);\n this.update(this.current);\n }\n}\n// Extend MPL widgets with backend specific methods\nextend(MPLSelectionWidget.prototype, MPLMethods);\nextend(MPLScrubberWidget.prototype, MPLMethods);\n\nwindow.HoloViews.MPLSelectionWidget = MPLSelectionWidget\nwindow.HoloViews.MPLScrubberWidget = MPLScrubberWidget\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in HoloViews.kernels) && (HoloViews.kernels[plot_id])) {\n HoloViews.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.HoloViews.comms) {\n return HoloViews.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in HoloViews.kernels) && (HoloViews.kernels[plot_id])) {\n var comm = HoloViews.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n }\n HoloViews.comms[comm_id] = comm;\n return comm;\n }\n\n window.HoloViews.comm_manager = new JupyterCommManager();\n " }, "metadata": {}, "output_type": "display_data" @@ -1144,215 +1488,109 @@ "outputs": [ { "data": { - "text/html": [ - "
\n", - "
\n", - "
\n", + "application/javascript": [ + "\n", + "// Ugly hack - see #2574 for more information\n", + "if (!(document.getElementById('4838369880')) && !(document.getElementById('_anim_img243df457cda84b4b9d292c76dcc9d1d3'))) {\n", + " console.log(\"Creating DOM nodes dynamically for assumed nbconvert export. To generate clean HTML output set HV_DOC_HTML as an environment variable.\")\n", + " var htmlObject = document.createElement('div');\n", + " htmlObject.innerHTML = `
\n", + "
\n", + "
\n", " \n", " \n", " \n", "
\n", "
\n", - "
\n", - "
\n", + "
\n", + " \n", " \n", " \n", "
\n", - "
\n", - "\t \n", - " \n", + " \n", " \n", " \n", "
\n", - "
\n", - "\n", - "\n", - "" + "var widget_ids = new Array(1);\n", + "\n", + "\n", + "widget_ids[0] = \"_anim_widget243df457cda84b4b9d292c76dcc9d1d3_out\";\n", + "\n", + "\n", + "function create_widget() {\n", + " var frame_data = {\"0\": \"\", \"1\": \"\", \"2\": \"\", \"3\": \"\", \"4\": \"\", \"5\": \"\", \"6\": \"\", \"7\": \"\", \"8\": \"\", \"9\": \"\", \"10\": \"\"};\n", + " var dim_vals = ['0.0'];\n", + " var keyMap = {\"('0.0',)\": 0, \"('100000.0',)\": 1, \"('200000.0',)\": 2, \"('300000.0',)\": 3, \"('400000.0',)\": 4, \"('500000.0',)\": 5, \"('600000.0',)\": 6, \"('700000.0',)\": 7, \"('800000.0',)\": 8, \"('900000.0',)\": 9, \"('1000000.0',)\": 10};\n", + " var notFound = \"

\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + "
\n", + "

" ], "text/plain": [ ":HoloMap [out]\n", @@ -1360,7 +1598,11 @@ ] }, "execution_count": 14, - "metadata": {}, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": 4838369880 + } + }, "output_type": "execute_result" } ], @@ -1415,9 +1657,9 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZwAAAEKCAYAAAAmfuNnAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XmYFNW9//H3d1Z2mJFhHTYVVBZFmSCIiYlhM1FR43pV\nyPbjajTxmhVjcomaa8x249XkargkEY1r3CAaRSSLMYIyKDsioCADyOKw7zDf3x99BpuxZ+2Zrpnp\nz+t56umq06dOnWqb+dhVp6rM3REREWloGVF3QERE0oMCR0REUkKBIyIiKaHAERGRlFDgiIhISihw\nREQkJRQ4IiKSEgocERFJCQWOiIikRFbUHYhax44dvXfv3lF3Q0SkSZk/f/5Wdy+ozTppHzi9e/em\nuLg46m6IiDQpZra2tuvokJqIiKSEAkdERFJCgSMiIimhwBERkZRQ4IiISEoocEREJCUUOCIikhJJ\nBY6Z5ZvZLDNbGV7zKqk3IdRZaWYT4sqHmNliM1tlZveYmVXVrpldbWaLwvSamZ0W19aa0NYCM2vQ\nC2vcnSfmrWPWsk0NuRkRkWYl2V84k4DZ7t4XmB2Wj2Fm+cBk4ExgKDA5LpjuAyYCfcM0tpp23wPO\ncfdTgTuAKRU29xl3H+zuRUnuV5UOlzkPzl3D955axOZd+xtyUyIizUaygTMOmBbmpwEXJagzBpjl\n7qXuvg2YBYw1s65AO3ef4+4OPBi3fsJ23f210AbAXKAwyf7XSXZmBndfMZg9Bw7zvScXEeu+iIhU\nJdnA6ezuGwHCa6cEdboD6+KWS0JZ9zBfsbym7X4FeCFu2YGXzGy+mU2sw77Uyomd2nLLeSfztxVb\neOSN9xt6cyIiTV6191Izs5eBLgneurWG27AEZV5FefUNmn2GWOCcHVc8wt03mFknYJaZve3ur1Sy\n/kRih/Lo2bNnTTaZ0PjhvZn99mZ+/Nxyhh9/HMcXtKlzWyIizV21v3DcfaS7D0wwTQc2hUNjhNfN\nCZooAXrELRcCG0J5YYJyqmrXzE4FpgLj3P3DuH5uCK+bgWeInS+qbJ+muHuRuxcVFNTqZqfHyMgw\nfn7paeRkZXDzEws5fKSszm2JiDR3yR5SmwGUjzqbAExPUGcmMNrM8sJggdHAzHCobJeZDQuj08bH\nrZ+wXTPrCTwNXOvu75RvwMxam1nb8vmwjSVJ7luNdGnfgv+6eCAL123nN39bnYpNiog0SckGzl3A\nKDNbCYwKy5hZkZlNBXD3UmIjyuaF6fZQBnA9sV8rq4DVfHROJmG7wH8CxwH/W2H4c2fgVTNbCLwB\nPO/uLya5bzV2/qnduGhwN+7560oWrtueqs2KiDQplu4jrIqKirw+noezY98hzrv7FVpkZ/LcN86m\nVU7aP2pIRJoxM5tf20tQdKeBetK+ZTa/uPw03t26h5/85e2ouyMi0ugocOrRWSd05Ktn9+GhuWv5\n24pE4ydERNKXAqeefXvMSZzUuS3ffXIRpXsORt0dEZFGQ4FTz1pkZ/KrKwazfe9Bvv/0Yt2FQEQk\nUOA0gP7d2vGt0Sfx4tIPePrN9VF3R0SkUVDgNJD/98njGdo7n8kzlrKudG/U3RERiZwCp4FkZhi/\nvDz29IRvPbGQI2U6tCYi6U2B04B65LfiRxcO4I01pUz957tRd0dEJFIKnAb2hTO6M3ZAF37x0gqW\nbdgZdXdERCKjwGlgZsadlwyiQ6scbn58AfsPHYm6SyIikVDgpEB+6xx+dumprNi0i1++tCLq7oiI\nREKBkyKfOakT1wzrydRX3+O11Vuj7o6ISMopcFLo+587hd7HtebbTyxkx75DUXdHRCSlFDgp1Con\ni19dMZhNuw7woxlLo+6OiEhKKXBSbHCPDnz93BN55q31PLdoQ/UriIg0EwqcCNzwmRM5rUcHbn1m\nCR/s2B91d0REUkKBE4HszAx+dflpHDxcxneeXEiZ7kIgImlAgROR4wvacOvnT+GfK7fy0Ny1UXdH\nRKTBKXAidPWZPfnMSQXc+ZflrNq8K+ruiIg0qKQDx8zyzWyWma0Mr3mV1JsQ6qw0swlx5UPMbLGZ\nrTKze8zMqmrXzD5tZjvMbEGY/jOurbFmtiK0NSnZfWtoZsZPLz2VVjmZ/MfjCzh4uCzqLomINJj6\n+IUzCZjt7n2B2WH5GGaWD0wGzgSGApPjguk+YCLQN0xja9DuP919cJhuD9vIBH4DnAf0B64ys/71\nsH8NqlPbFvzkkkEsWb+Te/+6MuruiIg0mPoInHHAtDA/DbgoQZ0xwCx3L3X3bcAsYKyZdQXaufsc\njz0a88G49WvSbryhwCp3f9fdDwKPhTYavbEDu3LpkEJ+87dVzF9bGnV3REQaRH0ETmd33wgQXjsl\nqNMdWBe3XBLKuof5iuXVtTvczBaa2QtmNqCabTQJky/oT7cOLbn58YXsOXA46u6IiNS7GgWOmb1s\nZksSTDX9BWEJyryK8qq8CfRy99OAe4Fnq9nGxztjNtHMis2seMuWLdVsLjXatsjmvy8fzLpte/nx\n88ui7o6ISL2rUeC4+0h3H5hgmg5sCofGCK+bEzRRAvSIWy4ENoTywgTlVNauu+90991h/i9Atpl1\nrGIbifZnirsXuXtRQUFBTT6ClBjaJ59//9QJPPrGOmYt2xR1d0RE6lV9HFKbAZSPOpsATE9QZyYw\n2szywmCB0cDMcKhsl5kNC6PTxsetn7BdM+sSN5JtaNiHD4F5QF8z62NmOcCVoY0m5Zuj+nFK13ZM\nemoRW3cfiLo7IiL1pj4C5y5glJmtBEaFZcysyMymArh7KXAHsVCYB9weygCuB6YCq4DVwAtVtQtc\nCiwxs4XAPcCVHnMYuJFYuC0HnnD3JneHzJysDO6+YjC7Dhxm0lOLiY2lEBFp+izd/6AVFRV5cXFx\n1N34mKn/fJcfP7+cuy4ZxJVDe0bdHRGRY5jZfHcvqs06utNAI/XlEX0464TjuP25Zaz9cE/U3RER\nSZoCp5HKyDB+cdlpZGYYNz++gMNHdBcCEWnaFDiNWLcOLfnxRQN58/3t/PaVd6PujohIUhQ4jdyF\np3Xj/FO78qtZ77C4ZEfU3RERqTMFTiNnZvz4ooF0bJPLfzz+FvsPHYm6SyIidaLAaQI6tMrh55ed\nyuote7jrhbej7o6ISJ0ocJqIT/Yt4Itn9eaB19bwz5WN43Y8IiK1ocBpQiaddzIndmrDt/+0kO17\nD0bdHRGRWlHgNCEtsjO5+4rBfLj7ILc+u0R3IRCRJkWB08QM7N6em0f14/lFG5m+IOG9SUVEGiUF\nThN03TknMKRXHj+cvoT12/dF3R0RkRpR4DRBmRnGry4fTFmZ8+0nFlJWpkNrItL4KXCaqJ7HteI/\nL+jPnHc/5IHX1kTdHRGRailwmrDLi3rw6ZMK+OVLK9igQ2si0sgpcJowM+OOcQM54s7kGU3u0T8i\nkmYUOE1cj/xW3DyyH7OWbWLm0g+i7o6ISKUUOM3Al8/uw8ld2jJ5+lJ2HzgcdXdERBJS4DQD2ZkZ\n3HnJIDbt2s8vX1oRdXdERBJS4DQTZ/TM45ozezHttTUsKtkedXdERD4mqcAxs3wzm2VmK8NrXiX1\nJoQ6K81sQlz5EDNbbGarzOweM7Oq2jWz75jZgjAtMbMjZpYf3lsT2lpgZsXJ7FdT9Z2xJ3Fcm1y+\n/8xiPSFURBqdZH/hTAJmu3tfYHZYPkYIhMnAmcBQYHJcMN0HTAT6hmlsVe26+8/dfbC7DwZuAf7h\n7qVxm/tMeL8oyf1qktq1yOZHFwxgyfqdTJuzNuruiIgcI9nAGQdMC/PTgIsS1BkDzHL3UnffBswC\nxppZV6Cdu8/x2F0oH4xbvybtXgU8mmT/m53PDerCuSd34pcvrdBtb0SkUUk2cDq7+0aA8NopQZ3u\nwLq45ZJQ1j3MVyyvtl0za0Xs19BTccUOvGRm881sYp33qIkzM267cADuMHm67igtIo1HtYFjZi+H\n8yUVp3E13IYlKPMqymviAuBfFQ6njXD3M4DzgBvM7FOVdshsopkVm1nxli3N72FmPfJbcfOovry8\nfDMzl26KujsiIkANAsfdR7r7wATTdGBTODRGeN2coIkSoEfcciGwIZQXJiinBu1eSYXDae6+Ibxu\nBp4hdr6osn2a4u5F7l5UUFBQ1e43WV8a0YdTurbjRzOWsmv/oai7IyKS9CG1GUD5qLMJwPQEdWYC\no80sLwwWGA3MDIfKdpnZsDA6bXzc+pW2a2btgXMqlLU2s7bl82EbS5LctyYtOzODOy8eGK7NeSfq\n7oiIJB04dwGjzGwlMCosY2ZFZjYVIBz2ugOYF6bb4w6FXQ9MBVYBq4EXqmo3uBh4yd33xJV1Bl41\ns4XAG8Dz7v5ikvvW5J3eM49rh/Vi2pw1LFyna3NEJFqW7ieVi4qKvLi4+V62s3P/IUb+8h8UtM1l\n+g0jyMrUtb4ikjwzm1/bS1D016eZa9cim9suHMDSDTv13BwRiZQCJw2MHdiFz57ciV++9A4l2/ZG\n3R0RSVMKnDRgZtw2bgAAk6cv1bU5IhIJBU6aKMxrxTdH9WP225v13BwRiYQCJ418aURvTunajsm6\nNkdEIqDASSNZmRn85JJBbN51QNfmiEjKKXDSzOAeHZgwvLeuzRGRlFPgpKFvje5Hp7a53PK0npsj\nIqmjwElDbcO1Ocs27uQP/1oTdXdEJE0ocNLUmAFdGHlKJ/57lq7NEZHUUOCkqdi1OQMxg//UtTki\nkgIKnDTWvUNLvjmqH399ezMvLtG1OSLSsBQ4ae6LZ/VmQLfYtTk7dW2OiDQgBU6aK782Z+vuA/xy\n5oqouyMizZgCRzi1sAPjh/fmwblreev9bVF3R0SaKQWOALFrczq3bcEtTy/mkK7NEZEGoMARIHZt\nzo8uHMDbH+ziD/96L+ruiEgzpMCRo8YM6MzIUzrzq1krWVeqa3NEpH4pcOSo8ufmxK7NWaJrc0Sk\nXiUdOGaWb2azzGxleM2rpN6EUGelmU2IKx9iZovNbJWZ3WNmFsovM7OlZlZmZkUV2rol1F9hZmPi\nyseGslVmNinZfUtH5dfm/G3FFl7QtTkiUo/q4xfOJGC2u/cFZoflY5hZPjAZOBMYCkyOC6b7gIlA\n3zCNDeVLgEuAVyq01R+4EhgQ6v6vmWWaWSbwG+A8oD9wVagrtfTFs3ozsHs7fqRrc0SkHtVH4IwD\npoX5acBFCeqMAWa5e6m7bwNmAWPNrCvQzt3neOz4zYPl67v7cndPdGHIOOAxdz/g7u8Bq4iF2FBg\nlbu/6+4HgcdCXamlrMwMfnLxqWzdfYCfv6hrc0SkftRH4HR2940A4bVTgjrdgXVxyyWhrHuYr1he\nlaraSlQudTCosD0TzurNH19fy5u6NkdE6kGNAsfMXjazJQmmmv6CsARlXkV5g7ZlZhPNrNjMirds\n2VLN5tLXt0afROe2Lfi+rs0RkXpQo8Bx95HuPjDBNB3YFA6NEV43J2iiBOgRt1wIbAjlhQnKq1JV\nW4nKE+3PFHcvcveigoKCajaXvtrkZnHbuNi1Ob9/VdfmiEhy6uOQ2gygfNTZBGB6gjozgdFmlhcG\nC4wGZoZDcLvMbFgYnTa+kvUrbu9KM8s1sz7EBhq8AcwD+ppZHzPLITawYEayO5fuxgzowqj+nfnV\ny+/o2hwRSUp9BM5dwCgzWwmMCsuYWZGZTQVw91LgDmKhMA+4PZQBXA9MJXbyfzXwQlj/YjMrAYYD\nz5vZzNDWUuAJYBnwInCDux9x98PAjcTCbTnwRKgrSbrtwgFkmunaHBFJiqX7H5CioiIvLi6OuhuN\n3u9ffY/bn1vGr//tdM4/tVvU3RGRiJnZfHcvqr7mR3SnAamRCWf1ZlD39tz252Xs2Kdrc0Sk9hQ4\nUiOZGcadFw/iw90H+PnMt6Pujog0QQocqbFBhe354ll9ePj195m/VtfmiEjtKHCkVr45uh9d2rXg\n1md0bY6I1I4CR2qlTW4Wt4Xn5vxO1+aISC0ocKTWRg/owpgBnblb1+aISC0ocKROfhSuzfnBs7o2\nR0RqRoEjddK1fUu+PeYk/vHOFp5btDHq7ohIE6DAkTobP1zX5ohIzSlwpM4yM4yfXDKI0j0H+NmL\nujZHRKqmwJGkDOzeni+NiF2bM2f1h1F3R0QaMQWOJO2bo/pxQkFrvv7om3ywY3/U3RGRRkqBI0lr\nnZvFb68dwr6DR7jhkTc5eFgXhIrIxylwpF6c2KktP7v0NOav3cadf1kedXdEpBFS4Ei9+fypXfnq\n2X144LU1TF+wPuruiEgjo8CRevW9805maJ98Jj21mLc/2Bl1d0SkEVHgSL3Kzszg1/92Om1bZHHd\nQ/PZuV/X54hIjAJH6l2nti34zdVnULJtH996YiFlZbr1jYgocKSBfKJ3Prd+/hRmLdvE/a+sjro7\nItIIJBU4ZpZvZrPMbGV4zauk3oRQZ6WZTYgrH2Jmi81slZndY2YWyi8zs6VmVmZmRXH1R5nZ/LDO\nfDM7N+69v5vZCjNbEKZOyeybJO+LZ/XmwtO68YuZK3h15daouyMiEUv2F84kYLa79wVmh+VjmFk+\nMBk4ExgKTI4LpvuAiUDfMI0N5UuAS4BXKjS3FbjA3QcBE4CHKrx/tbsPDtPmJPdNkmQWu/XNiZ3a\n8I3H3mLD9n1Rd0lEIpRs4IwDpoX5acBFCeqMAWa5e6m7bwNmAWPNrCvQzt3neOz+9g+Wr+/uy919\nRcWG3P0td98QFpcCLcwsN8l9kAbUOjeL+68ZwsHDZVz/8JscOHwk6i6JSESSDZzO7r4RILwmOozV\nHVgXt1wSyrqH+YrlNfUF4C13PxBX9odwOO2H5YfnEjGziWZWbGbFW7ZsqcUmpS6OL2jDLy47jYXr\ntnP7n5dF3R0RiUi1gWNmL5vZkgTTuBpuI9Effq+ivPoGzQYAPwX+Pa746nCo7ZNhuray9d19irsX\nuXtRQUFBTTYpSRo7sAvXnXMCD7/+Pn8qXlf9CiLS7GRVV8HdR1b2npltMrOu7r4xHCJLdN6kBPh0\n3HIh8PdQXlihfAPVMLNC4BlgvLsfHf7k7uvD6y4ze4TY+aIHq2tPUufbo/uxqGQ7P3h2Cf27tWNA\nt/ZRd0lEUijZQ2oziJ28J7xOT1BnJjDazPLCYIHRwMxwCG6XmQ0Lh7/GV7L+UWbWAXgeuMXd/xVX\nnmVmHcN8NnA+sYEH0ohkZWZwz1Wnk986h+v+OJ8de3VRqEg6STZw7gJGmdlKYFRYxsyKzGwqgLuX\nAncA88J0eygDuB6YCqwCVgMvhPUvNrMSYDjwvJnNDPVvBE4Eflhh+HMuMNPMFgELgPXA/yW5b9IA\nOrbJ5X+vPoMPduznPx5/SxeFiqQRiw0QS19FRUVeXFwcdTfSzkNz1/LDZ5dw88h+3DSyb9TdEZFa\nMrP57l5Ufc2P6E4DEolrzuzJJWd05+7Z7/D3FbpkSiQdKHAkEmbGf100iJO7tOOmxxawrnRv1F0S\nkQamwJHItMzJ5P5rzqDMnesfns/+Q7ooVKQ5U+BIpHod15q7rxjMkvU7+eGzS0j3c4oizZkCRyL3\n2VM6841zT+RP80t4bJ4uChVprhQ40ijcNLIfn+zbkcnTl7Jw3faouyMiDUCBI41CZoZxz5WnU9A2\nl689/Calew5G3SURqWcKHGk08lrncN81Z7Bl9wFueuwtjuiiUJFmRYEjjcqphR24Y9wA/rlyK3e/\n/E7U3RGReqTAkUbnik/05IqiHtz711W8vGxT1N0RkXqiwJFG6bZxAxjUvT03P7GANVv3RN0dEakH\nChxplFpkZ/K/V59BZoZx3R/ns++gLgoVaeoUONJo9chvxf9ceTorNu3i+88s1kWhIk2cAkcatXP6\nFXDzyH4889Z6/jh3bdTdEZEkKHCk0bvxMydy7smduP25Zcxfuy3q7ohIHSlwpNHLyDB+dflgurZv\nyQ0Pv8nW3Qei7pKI1IECR5qE9q2yue+aM9i29yBff+QtDh8pi7pLIlJLChxpMgZ0a8+dFw9izrsf\n8vOXVkTdHRGppaQCx8zyzWyWma0Mr3mV1JsQ6qw0swlx5UPMbLGZrTKze8zMQvllZrbUzMrMrCiu\nfm8z22dmC8J0f3VtSfPyhSGFXDOsJ7/9x7u8sHhj1N0RkVpI9hfOJGC2u/cFZoflY5hZPjAZOBMY\nCkyOC6b7gIlA3zCNDeVLgEuAVxJsc7W7Dw7TdXHllbUlzcwPz+/PaT068J0nF7F6y+6ouyMiNZRs\n4IwDpoX5acBFCeqMAWa5e6m7bwNmAWPNrCvQzt3neOwCiwfL13f35e5e42MmVbUlzU9uVib3XX0G\nOVkZXPfQfPYcOBx1l0SkBpINnM7uvhEgvHZKUKc7EP9UrZJQ1j3MVyyvTh8ze8vM/mFmn4zbRl3a\nkiaqW4eW3HvV6azespvvPbVIF4WKNAFZ1VUws5eBLgneurWG20h0LsWrKK/KRqCnu39oZkOAZ81s\nQG3bMrOJxA6/0bNnz2o2KY3ViBM78p0xJ/PTF9/m9J55fOXsPlF3SUSqUG3guPvIyt4zs01m1tXd\nN4bDWpsTVCsBPh23XAj8PZQXVijfUE1fDgAHwvx8M1sN9KttW+4+BZgCUFRUpP81bsKuO+d43np/\nG3f+ZTmDurdnaJ/8qLskIpVI9pDaDKB81NkEYHqCOjOB0WaWFwYLjAZmhkNwu8xsWBhRNr6S9Y8y\nswIzywzzxxMbHPBuXdqS5sHM+MXlp9EzvxU3PPImm3fuj7pLIlKJZAPnLmCUma0ERoVlzKzIzKYC\nuHspcAcwL0y3hzKA64GpwCpgNfBCWP9iMysBhgPPm9nMUP9TwCIzWwg8CVxXXVvS/LVrkc391wxh\n9/7DfO3hN9l7UIMIRBojS/eTrUVFRV5cXBx1N6Qe/HnhBr7x2Fuc1LktU64toudxraLukkizZWbz\n3b2o+pof0Z0GpNm44LRuPPCloWzYvo8Lf/Mqr67cGnWXRCSOAkealXP6FTDjxrPp1DaX8b9/nSmv\nrNaQaZFGQoEjzU7vjq155msjGN2/C3f+5W1uemyBnhgq0ggocKRZap2bxX3XnMG3R/fjz4s28IX7\nXqNk296ouyWS1hQ40myZGTee25ffTShi3ba9XPjrf/Haap3XEYmKAkeavXNP7sz0G0aQ3zqHa3/3\nBr979T2d1xGJgAJH0sLxBW145mtn8dmTO3HHc8v41hML2X9I53VEUkmBI2mjbbhA9OaR/Xj6rfVc\ndv8c1m/fF3W3RNKGAkfSSkaGcdPIvvzf+CLe27qHC+99ldff/TDqbomkBQWOpKVR/Tvz7A0jaN8y\nm6unvs6019bovI5IA1PgSNo6sVMbnr1xBOf0K2DyjKV898lFOq8j0oAUOJLW2rXI5v/GF/GNc0/k\nT/NLuGLKXDbu0HkdkYagwJG0l5FhfHP0Sdx/zRBWbdrFBff+i3lrSqtfUURqRYEjEowd2IVnbhhB\nm9xMrpoylz/OXRt1l0SaFQWOSJx+ndsy/YazObtvR37w7BJueXoRBw7rvI5IfVDgiFTQvlU2v5vw\nCb726RN49I11XDVlLpv0JFGRpClwRBLIzDC+O/ZkfvNvZ7B84y4uuPdV5q/dFnW3RJo0BY5IFT5/\naleeueEsWmRncuWUOTz2xvtRd0mkyVLgiFTj5C7tmHHjCIYdfxyTnl7MD55dzMHDZVF3S6TJSSpw\nzCzfzGaZ2crwmldJvQmhzkozmxBXPsTMFpvZKjO7x8wslF9mZkvNrMzMiuLqX21mC+KmMjMbHN77\nu5mtiHuvUzL7JhKvQ6scHvjSUP79nOP549z3uXrqXDbv0nkdkdpI9hfOJGC2u/cFZoflY5hZPjAZ\nOBMYCkyOC6b7gIlA3zCNDeVLgEuAV+LbcveH3X2wuw8GrgXWuPuCuCpXl7/v7puT3DeRY2RmGLec\ndwr3XHU6i9fv4MJ7/8WCdduj7pZIk5Fs4IwDpoX5acBFCeqMAWa5e6m7bwNmAWPNrCvQzt3neOwm\nVg+Wr+/uy919RTXbvgp4NMn+i9Tahad146nrzyIr07j8t3N4onhd1F0SaRKSDZzO7r4RILwmOozV\nHYj/F1kSyrqH+YrlNXUFHw+cP4TDaT8sPzwn0hAGdGvPjBvPpqhXHt99chGTpy/h0BGd1xGpSrWB\nY2Yvm9mSBNO4Gm4j0R9+r6K8+gbNzgT2uvuSuOKr3X0Q8MkwXVvF+hPNrNjMirds2VKTTYp8TH7r\nHB788lC+enYfps1Zy9VTX2fr7gNRd0uk0ao2cNx9pLsPTDBNBzaFQ2OE10TnTUqAHnHLhcCGUF6Y\noLwmrqTCrxt3Xx9edwGPEDtfVNk+TXH3IncvKigoqOEmRT4uKzODH5zfn7uvGMzCddu58N5XWVyy\nI+puiTRKyR5SmwGUjzqbAExPUGcmMNrM8sJggdHAzHAIbpeZDQuHv8ZXsv4xzCwDuAx4LK4sy8w6\nhvls4HxiAw9EUuKi07vz1PVnAXDp/a/x6Bvv6xCbSAXJBs5dwCgzWwmMCsuYWZGZTQVw91LgDmBe\nmG4PZQDXA1OBVcBq4IWw/sVmVgIMB543s5lx2/wUUOLu78aV5QIzzWwRsABYD/xfkvsmUisDu7fn\nz18/m9N7duCWpxdz9k//yt0vv8Nm3RZHBABL96ccFhUVeXFxcdTdkGbkSJnz9xWbeXDOWv7xzhay\nMowxA7swflgvhvbJR+NZpDkws/nuXlR9zY9kNVRnRNJVZobx2VM689lTOrNm6x7+OHctTxSv4/lF\nGzm5S1uuHd6LiwZ3p3Wu/vlJetEvHP3CkRTYd/AI0xes58E5a1m2cSdtc7P4wpBCrh3eixMK2kTd\nPZFaq8svHAWOAkdSyN158/1tPDhnLX9ZvJFDR5yzT+zItcN78dmTO5GVqdsbStOgwKkDBY5EZcuu\nAzw+730efv19Nu7YT7f2Lbh6WC+u+EQPOrbJjbp7IlVS4NSBAkeidvhIGS8v38SDc9by2uoPycnM\n4POnduXa4b04vUcHDTKQRkmBUwcKHGlMVm3exUNz1vLUm+vZfeAwA7u3Y/yw3lw4uBstsjOj7p7I\nUQqcOlDR8D6UAAAMkElEQVTgSGO0+8BhnnlrPQ/NWcM7m3bTvmU2lxcVcs2wXvQ6rnXU3RNR4NSF\nAkcaM3fn9fdKeWjOWl5c+gFl7pzTr4Dxw3vx6X6dyMjQ4TaJhgKnDhQ40lRs2rmfR15/n0feeJ8t\nuw7QM78V1wzryeVFPejQKifq7kmaUeDUgQJHmpqDh8uYufQDHpqzljfWlJKblcGFp3Vj/PDeDCps\nH3X3JE0ocOpAgSNN2fKNO3lo7lqeeXM9+w4dYXCPDowf3ovPn9qV3CwNMpCGo8CpAwWONAc79x/i\nqfklPDRnLe9u3UN+6xyu+EQPxg3uxokFbXRBqdQ7BU4dKHCkOSkrc/61eisPzlnL7OWbKHPIzcrg\n5C5t6d+tPf27tWNAt3ac0qUdLXP0C0jqToFTBwocaa42bN/H6+99yLINO1kaph37DgGQYdCnY2sG\ndGvPgG7tQhC1J7+1Bh9IzShw6kCBI+nC3Vm/fd8xAbR8407Wb993tE7X9i1iAdS1Hf1DGBXmtdTd\nDuRj9HgCEamUmVGY14rCvFaMHtDlaPm2PQdZtnFnCKIdLN2wk7++vZmy8P+i7VpkHf0FVP5r6ISC\nNmTrvJDUkgJHJM3ltc5hxIkdGXFix6Nl+w4eYcWmXUcDaNmGnTz8+lr2H4o9Njun/LxQ13YhhNpz\nSte2tMrRnxSpnL4dIvIxLXMyGdyjA4N7dDhadvhIGe9t3cOyjeWH5Hbw4tIPeGzeOgAs7rxQeRAN\n6NaO43TnawkUOCJSI1mZGfTt3Ja+ndsybnB3IHZeaOOO/UcDaNmGnby5dht/Xrjh6Hqd2+XSI68V\nea1zyG+VE3ttnU1eqxyOa5NDXqsc8lvHytvmZul8UTOWdOCYWT7wONAbWANc7u7bEtSbAPwgLP7Y\n3aeF8iHAA0BL4C/ATe7uZvZz4ALgILAa+JK7bw/r3AJ8BTgCfMPdZ4byscD/AJnAVHe/K9n9E5HK\nmRndOrSkW4eWjOrf+Wj59r0fnRdatmEnH+zcz7rSvSwq2U7pnoMcOpJ4sFJWhsUFU3YsiMoDKUFA\n5bfK0fDuJiTpUWpm9jOg1N3vMrNJQJ67f69CnXygGCgCHJgPDHH3bWb2BnATMJdY4Nzj7i+Y2Wjg\nr+5+2Mx+CuDu3zOz/sCjwFCgG/Ay0C9s6h1gFFACzAOucvdlVfVfo9REUsvd2XPwCKW7D1K69yDb\n9hykdM9Btu2t8Lrn0NH3t+09eHQQQ0UtsjPifjkdG1D5rbPJb51LXvhF1TI7k9zsDHKzMsnJyiA3\nK4OsDNOvqjqIapTaOODTYX4a8HfgexXqjAFmuXspgJnNAsaa2d+Bdu4+J5Q/CFwEvODuL8WtPxe4\nNG57j7n7AeA9M1tFLHwAVrn7u6Gtx0LdKgNHRFLLzGiTm0Wb3Cx6HteqRuuUlTk79x86Gkgf7i4P\npkNxARULsHWleyndc5Cd+w/XqO0MI4TPRyGUm5VBTlZmeC0vy4x7L5RlZ5KTWUlZdkZ4rdhOBhlm\nZJiRmWGYccx8ZngvI8PIMMjMiC1XfK8pqo/A6ezuGwHcfaOZdUpQpzuwLm65JJR1D/MVyyv6MrHD\nduVtza1knYrbOLOG+yAijVhGhtGhVU6t7op96EgZ2/ceG0j7Dh3h4OEyDhwuC69H4uY/Wj6m7NAR\ndh84zIe7D3LwSKhzqCw2fyi2XNmvr4ZUHkZmIZgSBFX8exZCLcMIdYznvn52Sh/sV6PAMbOXgS4J\n3rq1httJFMdeRXn8tm8FDgMPV9NWoosCEn4NzGwiMBGgZ8+eiXssIk1admYGBW1zKWjb8KPkDh8p\n+1hwVRVi7s6RMqfMY7/eyjw2f8S90vfK3Ckrc4541e+5E9Y/tl7F99xjoZRKNQocdx9Z2XtmtsnM\nuoZfN12BzQmqlfDRYTeAQmKH3krCfHz50eEtYaDB+cBn/aOTTSVAj0rWqay84v5MAaZA7BxOZfsm\nIlITWZkZZGVm0FojwKtUH5cKzwAmhPkJwPQEdWYCo80sz8zygNHAzHAobpeZDbPYWbvx5euHEWff\nAy50970VtnelmeWaWR+gL/AGsUECfc2sj5nlAFeGuiIi0gjUxzmcu4AnzOwrwPvAZQBmVgRc5+5f\ndfdSM7uDWCgA3F4+gAC4no+GRb8QJoBfA7nArDCCZK67X+fuS83sCWKDAQ4DN7j7kbDNG4mFWybw\ne3dfWg/7JyIi9UA379SwaBGRWqvLsGjdfU9ERFJCgSMiIimhwBERkZRQ4IiISEoocEREJCXSfpSa\nmW0B1kbdjyR1BLZG3YlGQp/FsfR5HEufx0eS/Sx6uXtBbVZI+8BpDsysuLbDE5srfRbH0udxLH0e\nH4nis9AhNRERSQkFjoiIpIQCp3mYEnUHGhF9FsfS53EsfR4fSflnoXM4IiKSEvqFIyIiKaHAiZCZ\n9TCzv5nZcjNbamY3hfJ8M5tlZivDa14oNzO7x8xWmdkiMzsjrq0Jof7K8Byh8vIhZrY4rHNPeAxE\npduImpllmtlbZvZcWO5jZq+Hfj4eHj1BeDzF42G/Xjez3nFt3BLKV5jZmLjysaFslZlNiitPuI2o\nmVkHM3vSzN4O35Hhaf7duDn8O1liZo+aWYt0+X6Y2e/NbLOZLYkri+y7UNU2quThCXOaUj8BXYEz\nwnxb4B2gP/AzYFIonwT8NMx/jtjjGwwYBrweyvOBd8NrXpjPC++9AQwP67wAnBfKE24j6gn4JvAI\n8FxYfgK4MszfD1wf5r8G3B/mrwQeD/P9gYXEHm3RB1hN7HEVmWH+eCAn1Olf1TainoBpwFfDfA7Q\nIV2/G8QeI/8e0DLuv9kX0+X7AXwKOANYElcW2Xehsm1Uux9Rf5E0HfOlmg6MAlYAXUNZV2BFmP8t\ncFVc/RXh/auA38aV/zaUdQXejis/Wq+ybUS8/4XAbOBc4LnwZd4KZIX3hxN7cB/Enns0PMxnhXoG\n3ALcEtfmzLDe0XVD+S1hqnQbEX8W7Yj9gbUK5en63egOrAt/LLPC92NMOn0/gN4cGziRfRcq20Z1\n+6BDao1E+Ml/OvA60NljT0MlvHYK1cr/0ZUrCWVVlZckKKeKbUTpbuC7QFlYPg7Y7u6Hw3J8/4/u\nc3h/R6hf28+oqm1E6XhgC/AHix1inGpmrUnT74a7rwd+QewhjxuJ/feeT/p+PyDa70JlbVVJgdMI\nmFkb4CngP9x9Z1VVE5R5HcobHTM7H9js7vPjixNU9Wreay6fURaxQyj3ufvpwB5ihzQq01z2O6Fw\n7mAcscNg3YDWwHkJqqbL96MqqdjHOn0uCpyImVk2sbB52N2fDsWbzKxreL8rsDmUlwA94lYvBDZU\nU16YoLyqbURlBHChma0BHiN2WO1uoIOZlT8KPb7/R/c5vN8eKKX2n9HWKrYRpRKgxN1fD8tPEgug\ndPxuAIwE3nP3Le5+CHgaOIv0/X5AtN+FytqqkgInQmEkyO+A5e7+33FvzQDKR5BMIHZup7x8fBgh\nMgzYEX7mzgRGm1le+D/B0cSOM28EdpnZsLCt8RXaSrSNSLj7Le5e6O69iZ3k/au7Xw38Dbg0VKv4\nWZT3/9JQ30P5lWGUUh+gL7ETovOAvmHEUU7YxoywTmXbiIy7fwCsM7OTQtFngWWk4XcjeB8YZmat\nQn/LP4+0/H4EUX4XKttG1aI4+aXp6Im2s4n9DF0ELAjT54gdN54NrAyv+aG+Ab8hNppmMVAU19aX\ngVVh+lJceRGwJKzzaz662DfhNhrDBHyaj0apHU/sD8Iq4E9AbihvEZZXhfePj1v/1rC/KwijbUL5\n54iNBFwN3BpXnnAbUU/AYKA4fD+eJTayKG2/G8BtwNuhzw8RG2mWFt8P4FFi564OEft18ZUovwtV\nbaOqSXcaEBGRlNAhNRERSQkFjoiIpIQCR0REUkKBIyIiKaHAERGRlFDgiDQRZvZFM+sWdT9E6kqB\nI9J0fJHYbV1EmiRdhyMSITP7JrGL8QCmErvA8zl3Hxje/zbQhthFeQ8A64F9xO6EvC/lHRZJgn7h\niETEzIYAXwLOJPZMkf9H7G4CH+PuTxK768DV7j5YYSNNUVb1VUSkgZwNPOPuewDM7Gngk9F2SaTh\n6BeOSHQS3eK9A8f+u2yRor6INDgFjkh0XgEuCndAbg1cTOyxvZ3M7DgzywXOj6u/i9ijyEWaJB1S\nE4mIu79pZg8QuxMxwFR3n2dmtxN78ut7xO6OXO4B4H4z06ABaZI0Sk1ERFJCh9RERCQlFDgiIpIS\nChwREUkJBY6IiKSEAkdERFJCgSMiIimhwBERkZRQ4IiISEr8f6MPIFYLyKXYAAAAAElFTkSuQmCC\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1450,7 +1692,7 @@ "data": { "image/png": "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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1487,15 +1729,11 @@ " * out (out) float64 0.0 1e+05 2e+05 3e+05 4e+05 ...\n", "Dimensions without coordinates: x, y\n", "Data variables:\n", - " grid__x_length float64 1e+05\n", - " grid__x_origin float64 0.0\n", " grid__x_size int64 101\n", - " grid__x_spacing float64 1e+03\n", - " grid__y_length float64 1e+05\n", - " grid__y_origin float64 0.0\n", " grid__y_size int64 101\n", - " grid__y_spacing float64 1e+03\n", - " topography__elevation (y, x) float64 0.2121 0.1451 0.7867 0.614 ...\n", + " grid__x_length float64 1e+05\n", + " grid__y_length float64 1e+05\n", + " topography__elevation (y, x) float64 0.7405 0.8413 0.5533 0.06639 ...\n", " flow_routing__pit_method " + "" ] }, "metadata": {}, @@ -1623,20 +1861,16 @@ " * y (y) float64 0.0 1e+03 2e+03 3e+03 4e+03 5e+03 ...\n", " * spower__k_coef (spower__k_coef) float64 5e-05 6e-05 7e-05\n", "Data variables:\n", - " grid__x_length float64 1e+05\n", - " grid__x_origin float64 0.0\n", " grid__x_size int64 101\n", - " grid__x_spacing float64 1e+03\n", - " grid__y_length float64 1e+05\n", - " grid__y_origin float64 0.0\n", " grid__y_size int64 101\n", - " grid__y_spacing float64 1e+03\n", + " grid__x_length float64 1e+05\n", + " grid__y_length float64 1e+05\n", " flow_routing__pit_method \n", - "
\n", - "
\n", + "application/javascript": [ + "\n", + "// Ugly hack - see #2574 for more information\n", + "if (!(document.getElementById('4849944672')) && !(document.getElementById('_anim_img223e300e1980416d9c840b47a7f91119'))) {\n", + " console.log(\"Creating DOM nodes dynamically for assumed nbconvert export. To generate clean HTML output set HV_DOC_HTML as an environment variable.\")\n", + " var htmlObject = document.createElement('div');\n", + " htmlObject.innerHTML = `
\n", + "
\n", + "
\n", " \n", " \n", " \n", "
\n", "
\n", - "
\n", - "
\n", + "
\n", + " \n", " \n", " \n", "
\n", - "
\n", - "\t \n", - " \n", + " \n", " \n", " \n", "
\n", - "
\n", - "\t \n", - " \n", + " \n", " \n", " \n", "
\n", - "
\n", - "\n", - "\n", - "" + "var widget_ids = new Array(2);\n", + "\n", + "\n", + "widget_ids[0] = \"_anim_widget223e300e1980416d9c840b47a7f91119_out\";\n", + "\n", + "widget_ids[1] = \"_anim_widget223e300e1980416d9c840b47a7f91119_spower__k_coef\";\n", + "\n", + "\n", + "function create_widget() {\n", + " var frame_data = {\"0\": \"\", \"1\": \"\", \"2\": \"\", \"3\": \"\", \"4\": \"\", \"5\": \"\", \"6\": \"\", \"7\": \"\", \"8\": \"\", \"9\": \"\", \"10\": \"\", \"11\": \"\", \"12\": \"\", \"13\": \"\", \"14\": \"\", \"15\": \"\", \"16\": \"\", \"17\": \"\", \"18\": \"\", \"19\": \"\", \"20\": \"\", \"21\": \"\", \"22\": \"\", \"23\": \"\", \"24\": \"\", \"25\": \"\", \"26\": \"\", \"27\": \"\", \"28\": \"\", \"29\": \"\", \"30\": \"\", \"31\": \"\", \"32\": \"\"};\n", + " var dim_vals = ['0.0', '0.000050000'];\n", + " var keyMap = {\"('0.0', '0.000050000')\": 0, \"('0.0', '0.000060000')\": 1, \"('0.0', '0.000070000')\": 2, \"('100000.0', '0.000050000')\": 3, \"('100000.0', '0.000060000')\": 4, \"('100000.0', '0.000070000')\": 5, \"('200000.0', '0.000050000')\": 6, \"('200000.0', '0.000060000')\": 7, \"('200000.0', '0.000070000')\": 8, \"('300000.0', '0.000050000')\": 9, \"('300000.0', '0.000060000')\": 10, \"('300000.0', '0.000070000')\": 11, \"('400000.0', '0.000050000')\": 12, \"('400000.0', '0.000060000')\": 13, \"('400000.0', '0.000070000')\": 14, \"('500000.0', '0.000050000')\": 15, \"('500000.0', '0.000060000')\": 16, \"('500000.0', '0.000070000')\": 17, \"('600000.0', '0.000050000')\": 18, \"('600000.0', '0.000060000')\": 19, \"('600000.0', '0.000070000')\": 20, \"('700000.0', '0.000050000')\": 21, \"('700000.0', '0.000060000')\": 22, \"('700000.0', '0.000070000')\": 23, \"('800000.0', '0.000050000')\": 24, \"('800000.0', '0.000060000')\": 25, \"('800000.0', '0.000070000')\": 26, \"('900000.0', '0.000050000')\": 27, \"('900000.0', '0.000060000')\": 28, \"('900000.0', '0.000070000')\": 29, \"('1000000.0', '0.000050000')\": 30, \"('1000000.0', '0.000060000')\": 31, \"('1000000.0', '0.000070000')\": 32};\n", + " var notFound = \"

\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + "
\n", + "

" ], "text/plain": [ ":HoloMap [out,spower__k_coef]\n", @@ -2042,7 +2048,11 @@ ] }, "execution_count": 23, - "metadata": {}, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": 4849944672 + } + }, "output_type": "execute_result" } ], @@ -2058,7 +2068,7 @@ "source": [ "## Create an alternative version of the model\n", "\n", - "xarray-simlab makes it easy to create alternative versions of a model. In the example below, instead of using constant block uplift, we set a linear uplift function along the $x$ dimension. The first step is to create a new `Process` class. " + "xarray-simlab makes it easy to create alternative versions of a model. In the example below, instead of using constant block uplift, we set a linear uplift function along the $x$ dimension. The first step is to create a new process, i.e., a Python class decorated by ``xsimlab.process``. " ] }, { @@ -2069,29 +2079,31 @@ }, "outputs": [], "source": [ - "from xtopo.models.fastscape_base import StackedGridXY, BoundaryFacesXY\n", - "from xsimlab import Process, FloatVariable, Variable, ForeignVariable\n", + "from xtopo.models.fastscape_base import Grid2D, ClosedBoundaryFaces\n", "\n", "\n", - "class VariableUplift(xsimlab.Process):\n", + "@xs.process\n", + "class VariableUplift(object):\n", " \"\"\"Compute spatially variable uplift as a linear function of x.\"\"\"\n", " \n", - " x_coef = FloatVariable((), description='uplift function x coefficient')\n", - " active_nodes = ForeignVariable(BoundaryFacesXY, 'active_nodes')\n", - " x = ForeignVariable(StackedGridXY, 'x')\n", - " uplift = Variable((), provided=True, group='uplift')\n", + " x_coef = xs.variable(description='uplift function x coefficient')\n", + " \n", + " active_nodes = xs.foreign(ClosedBoundaryFaces, 'active_nodes')\n", + " x = xs.foreign(Grid2D, 'x')\n", + " \n", + " uplift = xs.variable(intent='out', group='uplift')\n", "\n", " def initialize(self):\n", - " mask = self.active_nodes.value\n", + " mask = self.active_nodes\n", " ny, nx = mask.shape\n", "\n", - " u_rate = np.ones((ny, nx)) * self.x_coef.value * self.x.value[None, :]\n", + " u_rate = np.ones((ny, nx)) * self.x_coef * self.x[None, :]\n", " \n", - " self.uplift.rate = np.zeros((ny, nx))\n", - " self.uplift.rate[mask] = u_rate[mask]\n", + " self._u_rate = np.zeros((ny, nx))\n", + " self._u_rate[mask] = u_rate[mask]\n", "\n", " def run_step(self, dt):\n", - " self.uplift.value = self.uplift.rate * dt\n" + " self.uplift = self._u_rate * dt\n" ] }, { @@ -2109,32 +2121,28 @@ { "data": { "text/plain": [ - "\n", + "\n", "grid\n", - " x_length (in) total grid length in x\n", - " x_origin (in) grid x-origin\n", - " x_size (in) nb. of nodes in x\n", - " x_spacing (in) node spacing in x\n", - " y_length (in) total grid length in y\n", - " y_origin (in) grid y-origin\n", - " y_size (in) nb. of nodes in y\n", - " y_spacing (in) node spacing in y\n", + " y_size [in] nb. of nodes in y\n", + " y_length [in] total grid length in y\n", + " x_size [in] nb. of nodes in x\n", + " x_length [in] total grid length in x\n", "boundaries\n", "flow_routing\n", - " pit_method (in) \n", + " pit_method [in]\n", "area\n", "spower\n", - " k_coef (in) stream-power constant\n", - " m_exp (in) stream-power drainage area exponent\n", - " n_exp (in) stream-power slope exponent\n", + " n_exp [in] stream-power slope exponent\n", + " k_coef [in] stream-power constant\n", + " m_exp [in] stream-power drainage area exponent\n", "diffusion\n", - " k_coef (in) diffusivity\n", + " k_coef [in] diffusivity\n", "erosion\n", "uplift_func\n", - " x_coef (in) uplift function x coefficient\n", + " x_coef [in] uplift function x coefficient\n", "uplift\n", "topography\n", - " elevation (in) topographic elevation" + " elevation [inout] ('y', 'x') topographic elevation" ] }, "execution_count": 25, @@ -2190,9 +2198,9 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\nAAAAAElFTkSuQmCC\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -2211,215 +2219,109 @@ "outputs": [ { "data": { - "text/html": [ - "
\n", - "
\n", - "
\n", + "application/javascript": [ + "\n", + "// Ugly hack - see #2574 for more information\n", + "if (!(document.getElementById('4846779416')) && !(document.getElementById('_anim_img1e0d684e6f4e43239ed8bf308478af1c'))) {\n", + " console.log(\"Creating DOM nodes dynamically for assumed nbconvert export. To generate clean HTML output set HV_DOC_HTML as an environment variable.\")\n", + " var htmlObject = document.createElement('div');\n", + " htmlObject.innerHTML = `
\n", + "
\n", + "
\n", " \n", " \n", " \n", "
\n", "
\n", - "
\n", - "
\n", + "
\n", + " \n", " \n", " \n", "
\n", - "
\n", - "\t \n", - " \n", + " \n", " \n", " \n", "
\n", - "
\n", - "\n", - "\n", - "" + "var widget_ids = new Array(1);\n", + "\n", + "\n", + "widget_ids[0] = \"_anim_widget1e0d684e6f4e43239ed8bf308478af1c_out\";\n", + "\n", + "\n", + "function create_widget() {\n", + " var frame_data = {\"0\": \"\", \"1\": \"\", \"2\": \"\", \"3\": \"\", \"4\": \"\", \"5\": \"\", \"6\": \"\", \"7\": \"\", \"8\": \"\", \"9\": \"\", \"10\": \"\"};\n", + " var dim_vals = ['0.0'];\n", + " var keyMap = {\"('0.0',)\": 0, \"('100000.0',)\": 1, \"('200000.0',)\": 2, \"('300000.0',)\": 3, \"('400000.0',)\": 4, \"('500000.0',)\": 5, \"('600000.0',)\": 6, \"('700000.0',)\": 7, \"('800000.0',)\": 8, \"('900000.0',)\": 9, \"('1000000.0',)\": 10};\n", + " var notFound = \"

\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " \n", + "
\n", + "
\n", + "

" ], "text/plain": [ ":HoloMap [out]\n", @@ -2427,7 +2329,11 @@ ] }, "execution_count": 28, - "metadata": {}, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": 4846779416 + } + }, "output_type": "execute_result" } ], diff --git a/doc/faq.rst b/doc/faq.rst index 386d22f9..788fb80c 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -41,9 +41,9 @@ wrapping Fortran code or pybind11_ for wrapping C++11 code). As with any other framework, xarray-simlab introduces an overhead compared to a simple, straightforward (but non-flexible) implementation of a model. The preliminary benchmarks that we have run -show only a very small overhead, though. This overhead is mainly -introduced by the thin object-oriented layer that model components -(i.e., Python classes) together form. +show only a very small (almost free) overhead, though. This overhead +is mainly introduced by the thin object-oriented layer that model +components (i.e., Python classes) together form. .. _Cython: http://cython.org/ .. _Numba: http://numba.pydata.org/ diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 7a929eee..5a86da75 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -122,8 +122,8 @@ Regressions (will be fixed in future releases) are not handled by xarray-simlab yet. - Variables don't accept anymore a dimension that corresponds to their own name. This may be useful, e.g., for sensitivity analysis, but as - the latter is not implemented yet this feature will be added back in - a next release. + the latter is not implemented yet this feature has been removed and + will be added back in a next release. v0.1.1 (20 November 2017) -------------------------