8000 fix #832 : implemented TypedSession class (based on preliminary code … · larray-project/larray@60deff0 · GitHub
[go: up one dir, main page]

Skip to content

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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)
0