11
11
12
12
import numpy as np
13
13
14
+ from larray .core .abstractbases import ABCArray
14
15
from larray .core .metadata import Metadata
15
16
from larray .core .group import Group
16
- from larray .core .axis import Axis
17
+ from larray .core .axis import Axis , AxisCollection
17
18
from larray .core .constants import nan
18
19
from larray .core .array import Array , get_axes , ndtest , zeros , zeros_like , sequence , asarray
19
20
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):
1393
1394
return res
1394
1395
1395
1396
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
1416
1402
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).
1441
1403
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
+ """
1442
1409
Examples
1443
1410
--------
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
1460
1475
"""
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
-
1477
1476
def __setitem__ (self , key , value ):
1478
1477
self ._check_key_value (key , value )
1479
1478
@@ -1494,53 +1493,35 @@ def _check_key_value(self, key, value):
1494
1493
cls = self .__class__
1495
1494
attr_def = getattr (cls , key , None )
1496
1495
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 )
1517
1518
1518
1519
def copy (self ):
1519
1520
instance = self .__class__ ()
1520
1521
for key , value in self .items ():
1521
1522
instance [key ] = copy (value )
1522
1523
return instance
1523
1524
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
-
1544
1525
1545
1526
def _exclude_private_vars (vars_dict ):
1546
1527
return {k : v for k , v in vars_dict .items () if not k .startswith ('_' )}
0 commit comments