Skip to content

Commit 60deff0

Browse files
committed
fix #832 : implemented TypedSession class (based on preliminary code written by gdementen)
1 parent 226e2c0 commit 60deff0

File tree

5 files changed

+306
-590
lines changed

5 files changed

+306
-590
lines changed

doc/source/api.rst

Lines changed: 6 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -815,73 +815,21 @@ Load/Save
815815
Session.to_hdf
816816
Session.to_pickle
817817

818-
.. _api-frozensession:
819-
820-
FrozenSession
821-
=============
822-
823-
.. autosummary::
824-
:toctree: _generated/
825-
826-
FrozenSession
827-
828-
Exploring
829-
---------
830-
831-
.. autosummary::
832-
:toctree: _generated/
833-
834-
FrozenSession.names
835-
FrozenSession.keys
836-
FrozenSession.values
837-
FrozenSession.items
838-
FrozenSession.summary
839-
840-
Copying
841-
-------
842-
843-
.. autosummary::
844-
:toctree: _generated/
845-
846-
FrozenSession.copy
847-
848-
Testing
849-
-------
850-
851-
.. autosummary::
852-
:toctree: _generated/
853-
854-
FrozenSession.element_equals
855-
FrozenSession.equals
856-
857-
Selecting
858-
---------
859-
860-
.. autosummary::
861-
:toctree: _generated/
862-
863-
FrozenSession.get
864-
865-
Modifying
866-
---------
818+
ArrayDef
819+
========
867820

868821
.. autosummary::
869822
:toctree: _generated/
870823

871-
FrozenSession.apply
824+
ArrayDef
872825

873-
Load/Save
874-
---------
826+
TypedSession
827+
============
875828

876829
.. autosummary::
877830
:toctree: _generated/
878831

879-
FrozenSession.load
880-
FrozenSession.save
881-
FrozenSession.to_csv
882-
FrozenSession.to_excel
883-
FrozenSession.to_hdf
884-
FrozenSession.to_pickle
832+
TypedSession
885833

886834
.. _api-editor:
887835

larray/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
full_like, sequence, labels_array, ndtest, asarray, identity, diag,
1010
eye, all, any, sum, prod, cumsum, cumprod, min, max, mean, ptp, var,
1111
std, median, percentile, stack, zip_array_values, zip_array_items)
12-
from larray.core.session import Session, FrozenSession, local_arrays, global_arrays, arrays
12+
from larray.core.session import Session, TypedSession, ArrayDef, local_arrays, global_arrays, arrays
1313
from larray.core.constants import nan, inf, pi, e, euler_gamma
1414
from larray.core.metadata import Metadata
1515
from larray.core.ufuncs import wrap_elementwise_array_func, maximum, minimum, where
@@ -58,7 +58,7 @@
5858
'all', 'any', 'sum', 'prod', 'cumsum', 'cumprod', 'min', 'max', 'mean', 'ptp', 'var', 'std',
5959
'median', 'percentile', 'stack', 'zip_array_values', 'zip_array_items',
6060
# session
61-
'Session', 'FrozenSession', 'local_arrays', 'global_arrays', 'arrays',
61+
'Session', 'TypedSession', 'ArrayDef', 'local_arrays', 'global_arrays', 'arrays',
6262
# constants
6363
'nan', 'inf', 'pi', 'e', 'euler_gamma',
6464
# metadata

larray/core/session.py

Lines changed: 98 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
import numpy as np
1313

14+
from larray.core.abstractbases import ABCArray
1415
from larray.core.metadata import Metadata
1516
from larray.core.group import Group
16-
from larray.core.axis import Axis
17+
from larray.core.axis import Axis, AxisCollection
1718
from larray.core.constants import nan
1819
from larray.core.array import Array, get_axes, ndtest, zeros, zeros_like, sequence, asarray
1920
from larray.util.misc import float_error_handler_factory, is_interactive_interpreter, renamed_to, inverseop
@@ -1393,87 +1394,85 @@ def display(k, v, is_metadata=False):
13931394
return res
13941395

13951396

1396-
# XXX: I wonder if we shouldn't create an AbstractSession instead of defining the _disabled()
1397-
# private method below.
1398-
# Auto-completion on any instance of a class that inherits from FrozenSession
1399-
# should not propose the add(), update(), filter(), transpose() and compact() methods
1400-
class FrozenSession(Session):
1401-
"""
1402-
The purpose of the present class is to be inherited by user defined classes where parameters
1403-
and variables of a model are defined (see examples below). These classes will allow users to
1404-
benefit from the so-called 'autocomplete' feature from development software such as PyCharm
1405-
(see the Notes section below) plus the main features of the :py:obj:`Session()` objects.
1406-
1407-
After creating an instance of a user defined 'session', some restrictions will be applied on it:
1408-
1409-
- **it is not possible to add or remove any parameter or variable,**
1410-
- **all non array variables (axes, groups, ...) cannot be modified,**
1411-
- **only values of array variables can be modified, not their axes.**
1412-
1413-
The reason of the first restriction is to avoid to select a deleted variable or
1414-
to miss an added one somewhere in the code when using the 'autocomplete' feature.
1415-
In other words, users can safely rely on the 'autocomplete' to write the model.
1397+
class ArrayDef(ABCArray):
1398+
def __init__(self, axes):
1399+
if not all([isinstance(axis, (basestring, Axis)) for axis in axes]):
1400+
raise TypeError('ArrayDef only accepts string or Axis objects')
1401+
self.axes = axes
14161402

1417-
The reason of the second and third restrictions is to ensure the definition
1418-
of any variable or parameter to be constant throughout the whole code.
1419-
For example, a user don't need to remember that a new label has been added to
1420-
an axis of a given array somewhere earlier in the code (in a part of the model
1421-
written by a colleague). Therefore, these restrictions reduces the risk to deal
1422-
with unexpected error messages (like 'Incompatible Axes') and make it easier to
1423-
work in team.
1424-
1425-
Parameters
1426-
----------
1427-
filepath: str, optional
1428-
Path where items have been saved. This can be either the path to a single file, a path to
1429-
a directory containing .csv files or a pattern representing several .csv files.
1430-
meta : list of pairs or dict or OrderedDict or Metadata, optional
1431-
Metadata (title, description, author, creation_date, ...) associated with the array.
1432-
Keys must be strings. Values must be of type string, int, float, date, time or datetime.
1433-
1434-
Notes
1435-
-----
1436-
The 'autocomplete' is a feature in which a software predicts the rest of a variable or function
1437-
name after a user typed the first letters. This feature allows users to use longer but meaningful
1438-
variable or function names (like 'population_be' instead of 'pbe') and to avoid creating an unwanted
1439-
new variable by misspelling the name of a given variable (e.g. typing 'poplation = something'
1440-
('population' without u) will create a new variable instead of modifying the 'population' variable).
14411403

1404+
# XXX: the name of the class below is not really important since it will serve as base
1405+
# for the LazySession and users are expected to inherit from the LazySession when
1406+
# they define their own classes
1407+
class TypedSession(Session):
1408+
"""
14421409
Examples
14431410
--------
1444-
>>> class ModelVariables(FrozenSession):
1445-
... LAST_AGE = 120
1446-
... FIRST_OBS_YEAR = 1991
1447-
... LAST_PROJ_YEAR = 2070
1448-
... AGE = Axis('age=0..{}'.format(LAST_AGE))
1449-
... GENDER = Axis('gender=male,female')
1450-
... TIME = Axis('time={}..{}'.format(FIRST_OBS_YEAR, LAST_PROJ_YEAR))
1451-
... CHILDREN = AGE[0:17]
1452-
... ELDERS = AGE[65:]
1453-
... population = zeros((AGE, GENDER, TIME))
1454-
... births = zeros((AGE, GENDER, TIME))
1455-
... deaths = zeros((AGE, GENDER, TIME))
1456-
>>> m = ModelVariables()
1457-
>>> m.names # doctest: +NORMALIZE_WHITESPACE
1458-
['AGE', 'CHILDREN', 'ELDERS', 'FIRST_OBS_YEAR', 'GENDER', 'LAST_AGE', 'LAST_PROJ_YEAR', 'TIME', 'births',
1459-
'deaths', 'population']
1411+
Content of file 'model_variables.py'
1412+
1413+
>>> # ==== MODEL VARIABLES ====
1414+
>>> class ModelVariables(TypedSession):
1415+
... FIRST_OBS_YEAR = int
1416+
... FIRST_PROJ_YEAR = int
1417+
... LAST_PROJ_YEAR = int
1418+
... AGE = Axis
1419+
... GENDER = Axis
1420+
... TIME = Axis
1421+
... G_CHILDREN = Group
1422+
... G_ADULTS = Group
1423+
... G_OBS_YEARS = Group
1424+
... G_PROJ_YEARS = Group
1425+
... population = ArrayDef(('AGE', 'GENDER', 'TIME'))
1426+
... births = ArrayDef(('AGE', 'GENDER', 'TIME'))
1427+
... deaths = ArrayDef(('AGE', 'GENDER', 'TIME'))
1428+
1429+
Content of file 'model.py'
1430+
1431+
>>> def run_model(variant_name, first_proj_year, last_proj_year):
1432+
... # create an instance of the ModelVariables class
1433+
... m = ModelVariables()
1434+
... # ==== setup variables ====
1435+
... # set scalars
1436+
... m.FIRST_OBS_YEAR = 1991
1437+
... m.FIRST_PROJ_YEAR = first_proj_year
1438+
... m.LAST_PROJ_YEAR = last_proj_year
1439+
... # set axes
1440+
... m.AGE = Axis('age=0..120')
1441+
... m.GENDER = Axis('gender=male,female')
1442+
... m.TIME = Axis('time={}..{}'.format(m.FIRST_OBS_YEAR, m.LAST_PROJ_YEAR))
1443+
... # set groups
1444+
... m.G_CHILDREN = m.AGE[:17]
1445+
... m.G_ADULTS = m.AGE[18:]
1446+
... m.G_OBS_YEARS = m.TIME[:m.FIRST_PROJ_YEAR-1]
1447+
... m.G_PROJ_YEARS = m.TIME[m.FIRST_PROJ_YEAR:]
1448+
... # set arrays
1449+
... m.population = zeros((m.AGE, m.GENDER, m.TIME))
1450+
... m.births = zeros((m.AGE, m.GENDER, m.TIME))
1451+
... m.deaths = zeros((m.AGE, m.GENDER, m.TIME))
1452+
... # ==== model ====
1453+
... # some code here
1454+
... # ...
1455+
... # ==== output ====
1456+
... # save all variables in an HDF5 file
1457+
... m.save('{variant_name}.h5', display=True)
1458+
1459+
Content of file 'main.py'
1460+
1461+
>>> run_model('proj_2020_2070', first_proj_year=2020, last_proj_year=2070)
1462+
dumping FIRST_OBS_YEAR ... Cannot dump FIRST_OBS_YEAR. int is not a supported type
1463+
dumping FIRST_PROJ_YEAR ... Cannot dump FIRST_PROJ_YEAR. int is not a supported type
1464+
dumping LAST_PROJ_YEAR ... Cannot dump LAST_PROJ_YEAR. int is not a supported type
1465+
dumping AGE ... done
1466+
dumping GENDER ... done
1467+
dumping TIME ... done
1468+
dumping G_CHILDREN ... done
1469+
dumping G_ADULTS ... done
1470+
dumping G_OBS_YEARS ... done
1471+
dumping G_PROJ_YEARS ... done
1472+
dumping population ... done
1473+
dumping births ... done
1474+
dumping deaths ... done
14601475
"""
1461-
def __init__(self, filepath=None, meta=None):
1462-
# feed the kwargs dict with all items declared as class attributes
1463-
kwargs = {}
1464-
for key, value in vars(self.__class__).items():
1465-
if not key.startswith('_'):
1466-
kwargs[key] = value
1467-
1468-
if meta:
1469-
kwargs['meta'] = meta
1470-
1471-
Session.__init__(self, **kwargs)
1472-
object.__setattr__(self, 'add', self._disabled)
1473-
1474-
if filepath:
1475-
self.load(filepath)
1476-
14771476
def __setitem__(self, key, value):
14781477
self._check_key_value(key, value)
14791478

@@ -1494,53 +1493,35 @@ def _check_key_value(self, key, value):
14941493
cls = self.__class__
14951494
attr_def = getattr(cls, key, None)
14961495
if attr_def is None:
1497-
raise ValueError("The '{item}' item has not been found in the '{cls}' class declaration. "
1498-
"Adding a new item after creating an instance of the '{cls}' class is not permitted."
1499-
.format(item=key, cls=cls.__name__))
1500-
if (isinstance(value, (int, float, basestring, np.generic)) and value != attr_def) \
1501-
or (isinstance(value, (Axis, Group)) and not value.equals(attr_def)):
1502-
raise TypeError("The '{key}' item is of kind '{cls_name}' which cannot by modified."
1503-
.format(key=key, cls_name=attr_def.__class__.__name__))
1504-
if type(value) != type(attr_def):
1505-
raise TypeError("Expected object of type '{attr_cls}'. Got object of type '{value_cls}'."
1506-
.format(attr_cls=attr_def.__class__.__name__, value_cls=value.__class__.__name__))
1507-
if isinstance(attr_def, Array):
1508-
try:
1509-
attr_def.axes.check_compatible(value.axes)
1510-
except ValueError as e:
1511-
msg = str(e).replace("incompatible axes:", "Incompatible axes for array '{key}':".format(key=key))
1512-
raise ValueError(msg)
1513-
elif isinstance(value, np.ndarray) and value.shape != attr_def.shape:
1514-
raise ValueError("Incompatible shape for Numpy array '{key}'. "
1515-
"Expected shape {attr_shape} but got {value_shape}."
1516-
.format(key=key, attr_shape=attr_def.shape, value_shape=value.shape))
1496+
warnings.warn("'{}' is not declared in '{}'".format(key, self.__class__.__name__), stacklevel=2)
1497+
else:
1498+
attr_type = Array if isinstance(attr_def, ArrayDef) else attr_def
1499+
if not isinstance(value, attr_type):
1500+
raise TypeError("Expected object of type '{}'. Got object of type '{}'."
1501+
.format(attr_type.__name__, value.__class__.__name__))
1502+
if isinstance(attr_def, ArrayDef):
1503+
def get_axis(axis):
1504+
if isinstance(axis, basestring):
1505+
try:
1506+
axis = getattr(self, axis)
1507+
except AttributeError:
1508+
raise ValueError("Axis '{}' not defined in '{}'".format(axis, self.__class__.__name__))
1509+
return axis
1510+
1511+
defined_axes = AxisCollection([get_axis(axis) for axis in attr_def.axes])
1512+
try:
1513+
defined_axes.check_compatible(value.axes)
1514+
except ValueError as error:
1515+
msg = str(error).replace("incompatible axes:", "incompatible axes for array '{}':".format(key))\
1516+
.replace("vs", "was declared as")
1517+
raise ValueError(msg)
15171518

15181519
def copy(self):
15191520
instance = self.__class__()
15201521
for key, value in self.items():
15211522
instance[key] = copy(value)
15221523
return instance
15231524

1524-
def apply(self, func, *args, **kwargs):
1525-
kind = kwargs.pop('kind', Array)
1526-
instance = self.__class__()
1527-
for key, value in self.items():
1528-
instance[key] = func(value, *args, **kwargs) if isinstance(value, kind) else value
1529-
return instance
1530-
1531-
def _disabled(self, *args, **kwargs):
1532-
"""This method will not work because adding or removing item and modifying axes of declared arrays
1533-
is not permitted."""
1534-
raise ValueError(
1535-
"Adding or removing item and modifying axes of declared arrays is not permitted.".format(
1536-
cls=self.__class__.__name__
1537-
)
1538-
)
1539-
1540-
# XXX: not sure we should or not disable 'transpose()'?
1541-
__delitem__ = __delattr__ = _disabled
1542-
update = filter = transpose = compact = _disabled
1543-
15441525

15451526
def _exclude_private_vars(vars_dict):
15461527
return {k: v for k, v in vars_dict.items() if not k.startswith('_')}

larray/tests/data/test_session.h5

360 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)