Skip to content

Commit

Permalink
rework code (#3)
Browse files Browse the repository at this point in the history
* rework code

* Added unit handling

Co-authored-by: Eric Charles <[email protected]>
  • Loading branch information
eacharles and eacharles authored Feb 19, 2021
1 parent 635f7b5 commit ea9481e
Show file tree
Hide file tree
Showing 18 changed files with 570 additions and 475 deletions.
5 changes: 4 additions & 1 deletion python/cfgmdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
__version__ = get_git_version()
del get_git_version

from .unit import Unit
from .array import Array
from .property import Property
from .derived import Derived
from .configurable import Configurable
from .choice import Choice
from .parameter import Parameter
from .derived import Derived
from .model import Model
from .function import Function
23 changes: 23 additions & 0 deletions python/cfgmdl/array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python
""" Tools to manage array propery objects.
"""
from copy import deepcopy

import numpy as np

from .property import Property, defaults_decorator

class Array(Property):
"""Property sub-class numpy arrays
"""
defaults = deepcopy(Property.defaults)
defaults['dtype'] = (float, 'Data type of array element')
defaults['default'] = (np.nan, 'Default value')

@defaults_decorator(defaults)
def __init__(self, **kwargs):
super(Array, self).__init__(**kwargs)

def _cast_type(self, value):
"""Hook took override type casting"""
return np.array(value).astype(self.dtype)
9 changes: 4 additions & 5 deletions python/cfgmdl/choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ class Choice(Property):
"""

# Better to keep the structure consistent with Property
defaults = deepcopy(Property.defaults) + [
('choices', [], 'Allowed values'),
]
defaults = deepcopy(Property.defaults)
defaults['choices'] = ([], 'Allowed values')

# Overwrite the default dtype
idx = [d[0] for d in defaults].index('dtype')
defaults[idx] = ('dtype', str, 'Data type')
defaults['dtype'] = (str, 'Data type')

@defaults_decorator(defaults)
def __init__(self, **kwargs):
Expand Down
189 changes: 189 additions & 0 deletions python/cfgmdl/configurable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env python
"""
Classes used to describe aspect of Models.
The base class is `Property` which describes any one property of a model,
such as the name, or some other fixed property.
The `Parameter` class describes variable model parameters.
The `Derived` class describes model properies that are derived
from other model properties.
"""
from collections import OrderedDict as odict
from collections.abc import Mapping

from .property import Property
from .derived import Derived

from .utils import Meta, model_docstring

class Configurable:
"""Base class for Configurable objects
Examples::
# A simple class with some managed Properties
class MyClass(Configurable):
val_float = Property(dtype=float, default=0.0, help="A float value")
val_int = Property(dtype=int, default=1, help="An integer value")
val_required = Property(dtype=float, required=True, help="A float value")
val_str = Property(dtype=str, default="", help="A string value")
val_list = Property(dtype=list, default=[], help="An list value")
val_dict = Property(dtype=list, default={}, help="A dictionary value")
# A class with nested configuration
class MyPair(Model):
val_first = Property(dtype=MyClass, required=True, help="First MyClass object")
val_second = Property(dtype=MyClass, required=True, help="Second MyClass object")
# Default, all Properties take their default values (must provide required Properties)
my_obj = MyClass(val_required=3.)
# Access Properties
my_obj.val_float
my_obj.val_str
my_obj.val_list
my_obj.val_dict
my_obj.val_required
# Set Properties
my_obj.val_float = 5.4
my_obj.val_int = 3
my_obj.val_dict = dict(a=3, b=4)
# This will fail with a TypeError
my_obj.val_float = "not a float"
# Override values in construction
my_obj = MyClass(val_required=3., val_float=4.3, val_int=2, val_str="Hello World")
# Build nested Configurables
my_pair = MyPair(val_first=dict(val_required=3.), val_second=dict(val_required=4.))
my_pair.val_first.val_float
my_pair.val_second.val_int
"""

__metaclass__ = Meta

def __init__(self, **kwargs):
""" C'tor. Build from a set of keyword arguments.
"""
self._properties = self.find_properties()
self._init_properties(**kwargs)
self._cache()

@classmethod
def find_properties(cls):
"""Find properties that belong to this model"""
the_classes = cls.mro()
props = odict()
for the_class in the_classes:
for key, val in the_class.__dict__.items():
if isinstance(val, Property):
props[key] = val
return props

def __str__(self, indent=0):
""" Cast model as a formatted string
"""
try:
ret = '{0:>{2}}{1}'.format('', self.name, indent)
except AttributeError:
ret = "%s" % (type(self))
if not self._properties: #pragma: no cover
return ret
ret += '\n{0:>{2}}{1}'.format('', 'Parameters:', indent + 2)
width = len(max(self._properties.keys(), key=len))
for name in self._properties.keys():
value = getattr(self, name)
par = '{0!s:{width}} : {1!r}'.format(name, value, width=width)
ret += '\n{0:>{2}}{1}'.format('', par, indent + 4)
return ret

@classmethod
def defaults_docstring(cls, header=None, indent=None, footer=None):
"""Add the default values to the class docstring"""
return model_docstring(cls, header=header, indent=indent, footer=footer) #pragma: no cover

def update(self, *args, **kwargs):
"""
Set a group of attributes (parameters and members). Calls
`setp` directly, so kwargs can include more than just the
parameter value (e.g., bounds, free, etc.).
"""
if args:
raise ValueError("Argument passed to Model.upate() %s" % args)

kwargs = dict(kwargs)
for name, value in kwargs.items():
# Raise KeyError if Property not found
try:
prop = self._properties[name]
except KeyError as err:
raise KeyError("Warning: %s does not have properties %s" % (type(self), name)) from err

attr = getattr(self, '_%s' % name)
if isinstance(attr, Configurable):
# Set attributes
if isinstance(value, Mapping):
attr.update(**value)
else:
attr.update(value)
else:
prop.__set__(self, value)


def _init_properties(self, **kwargs):
""" Loop through the list of Properties,
extract the derived and required properties and do the
appropriate book-keeping
"""
missing = {}
kwcopy = kwargs.copy()
for k, p in self._properties.items():
if k not in kwcopy and p.required:
missing[k] = p

pval = kwcopy.pop(k, p.default)

p.__set__(self, pval)
if isinstance(p, Derived):
if p.loader is None:
p.loader = self.__getattribute__("_load_%s" % k)
elif isinstance(p.loader, str):
p.loader = self.__getattribute__(p.loader)
if not callable(p.loader):
raise ValueError("Callabe loader not defined for Derived object", type(self), k, p.loader)
if missing:
raise ValueError("One or more required properties are missing ", type(self), missing.keys())
if kwcopy:
raise KeyError("Warning: %s does not have attributes %s" % (type(self), kwcopy))

def todict(self):
""" Return self cast as an '~collections.OrderedDict' object
"""
return odict([(key, val.todict(self)) for key, val in self._properties.items()])


def _cache(self, name=None):
"""
Method called in to cache any computationally
intensive properties after updating the parameters.
Parameters
----------
name : string
The parameter name.
Returns
-------
None
"""
#pylint: disable=unused-argument, no-self-use
return
5 changes: 2 additions & 3 deletions python/cfgmdl/derived.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ class Derived(Property):
This allows specifying the specifying a 'loader' function by name
that is used to compute the value of the property.
"""
defaults = deepcopy(Property.defaults) + [
('loader', None, 'Function to load datum')
]
defaults = deepcopy(Property.defaults)
defaults['loader'] = (None, 'Function to load datum')

@classmethod
def dummy(cls): #pragma: no cover
Expand Down
4 changes: 3 additions & 1 deletion python/cfgmdl/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
def check_inputs(x, inputs):
"""Convert inputs to arrays"""
ret = []
if not isinstance(x, (Iterable, int, float)):
if x is None:
x = np.nan
elif not isinstance(x, (Iterable, int, float)):
raise TypeError("Non-numeric value %s passed in Physics" % (str(x)))

x = np.array(x).astype(np.float)
Expand Down
Loading

0 comments on commit ea9481e

Please sign in to comment.