1111
1212import numpy as np
1313
14+ from larray .core .abstractbases import ABCArray
1415from larray .core .metadata import Metadata
1516from larray .core .group import Group
16- from larray .core .axis import Axis
17+ from larray .core .axis import Axis , AxisCollection
1718from larray .core .constants import nan
1819from larray .core .array import Array , get_axes , ndtest , zeros , zeros_like , sequence , asarray
1920from 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
15451526def _exclude_private_vars (vars_dict ):
15461527 return {k : v for k , v in vars_dict .items () if not k .startswith ('_' )}
0 commit comments