-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Units system #160
base: main
Are you sure you want to change the base?
[WIP] Units system #160
Changes from all commits
2080ab1
7846618
d7b34ef
b560e45
5909129
26977e2
d2f5d2b
446b536
1985dd0
c1a7ac0
abc41ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,20 +6,20 @@ | |
from cyclus import lib | ||
|
||
from cymetric.tools import raw_to_series | ||
|
||
from cymetric import units | ||
|
||
METRIC_REGISTRY = {} | ||
|
||
|
||
def register_metric(cls): | ||
"""Adds a metric to the registry.""" | ||
METRIC_REGISTRY[cls.__name__] = cls | ||
|
||
if cls.registry and cls.registry is not NotImplemented: | ||
units.build_normalized_metric(cls) | ||
|
||
class Evaluator(object): | ||
"""An evaluation context for metrics.""" | ||
|
||
def __init__(self, db, write=True): | ||
def __init__(self, db, write=True, normed=True): | ||
"""Parameters | ||
---------- | ||
db : database | ||
|
@@ -40,22 +40,31 @@ def __init__(self, db, write=True): | |
self.recorder = rec = lib.Recorder(inject_sim_id=False) | ||
rec.register_backend(db) | ||
self.known_tables = db.tables | ||
self.set_norm = normed | ||
|
||
def get_metric(self, metric): | ||
def get_metric(self, metric, normed=False): | ||
"""Checks if metric is already in the registry; adds it if not.""" | ||
normed_name = "norm_" + metric | ||
if normed and normed_name in METRIC_REGISTRY: | ||
metric = normed_name | ||
if metric not in self.metrics: | ||
self.metrics[metric] = METRIC_REGISTRY[metric](self.db) | ||
return self.metrics[metric] | ||
|
||
def eval(self, metric, conds=None): | ||
def eval(self, metric, conds=None, normed=None): | ||
"""Evalutes a metric with the given conditions.""" | ||
normed_name = "norm_" + metric | ||
if (normed == True or (normed is None and self.set_norm == True)) and normed_name in METRIC_REGISTRY: | ||
metric = normed_name | ||
|
||
rawkey = (metric, conds if conds is None else frozenset(conds)) | ||
if rawkey in self.rawcache: | ||
return self.rawcache[rawkey] | ||
m = self.get_metric(metric) | ||
m = self.get_metric(metric, normed) | ||
frames = [] | ||
for dep in m.dependencies: | ||
frame = self.eval(dep, conds=conds) | ||
# norm=False to avoid inception | ||
frame = self.eval(dep, conds=conds, normed=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we force this to |
||
frames.append(frame) | ||
raw = m(frames=frames, conds=conds, known_tables=self.known_tables) | ||
if raw is None: | ||
|
@@ -81,3 +90,6 @@ def eval(metric, db, conds=None, write=True): | |
"""Evalutes a metric with the given conditions in a database.""" | ||
e = Evaluator(db, write=write) | ||
return e.eval(str(metric), conds=conds) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ class Metric(object): | |
"""Metric class""" | ||
dependencies = NotImplemented | ||
schema = NotImplemented | ||
registry = NotImplemented | ||
|
||
def __init__(self, db): | ||
self.db = db | ||
|
@@ -40,7 +41,7 @@ def name(self): | |
return self.__class__.__name__ | ||
|
||
|
||
def _genmetricclass(f, name, depends, scheme): | ||
def _genmetricclass(f, name, depends, scheme, register): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update docstring to describe |
||
"""Creates a new metric class with a given name, dependencies, and schema. | ||
|
||
Parameters | ||
|
@@ -59,8 +60,11 @@ class Cls(Metric): | |
dependencies = depends | ||
schema = scheme | ||
func = staticmethod(f) | ||
|
||
registry = register | ||
__doc__ = inspect.getdoc(f) | ||
|
||
def shema(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like this method name is probably a typo? And should this return |
||
return schema | ||
|
||
def __init__(self, db): | ||
"""Constructor for metric object in database.""" | ||
|
@@ -77,14 +81,15 @@ def __call__(self, frames, conds=None, known_tables=None, *args, **kwargs): | |
|
||
Cls.__name__ = str(name) | ||
register_metric(Cls) | ||
|
||
return Cls | ||
|
||
|
||
def metric(name=None, depends=NotImplemented, schema=NotImplemented): | ||
def metric(name=None, depends=NotImplemented, schema=NotImplemented,registry=NotImplemented): | ||
"""Decorator that creates metric class from a function or class.""" | ||
def dec(f): | ||
clsname = name or f.__name__ | ||
return _genmetricclass(f=f, name=clsname, scheme=schema, depends=depends) | ||
return _genmetricclass(f=f, name=clsname, scheme=schema, depends=depends, register=registry) | ||
return dec | ||
|
||
|
||
|
@@ -105,8 +110,9 @@ def dec(f): | |
('Units', ts.STRING), | ||
('Mass', ts.DOUBLE) | ||
] | ||
_matregistry = { "Mass": ["Units", "kg"]} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think I understand how the registry works? |
||
|
||
@metric(name='Materials', depends=_matdeps, schema=_matschema) | ||
@metric(name='Materials', depends=_matdeps, schema=_matschema, registry=_matregistry) | ||
def materials(rsrcs, comps): | ||
"""Materials metric returns the material mass (quantity of material in | ||
Resources times the massfrac in Compositions) indexed by the SimId, QualId, | ||
|
@@ -304,8 +310,9 @@ def agents(entry, exit, decom, info): | |
('Units', ts.STRING), | ||
('Quantity', ts.DOUBLE) | ||
] | ||
_transregistry = { "Quantity": ["Units", "kg"]} | ||
|
||
@metric(name='TransactionQuantity', depends=_transdeps, schema=_transschema) | ||
@metric(name='TransactionQuantity', depends=_transdeps, schema=_transschema, registry=_transregistry) | ||
def transaction_quantity(mats, tranacts): | ||
"""Transaction Quantity metric returns the quantity of each transaction throughout | ||
the simulation. | ||
|
@@ -400,7 +407,7 @@ def annual_electricity_generated_by_agent(elec): | |
'AgentId': elec.AgentId, | ||
'Year': elec.Time.apply(lambda x: x//12), | ||
'Energy': elec.Value.apply(lambda x: x/12)}, | ||
columns=['SimId', 'AgentId', 'Year', 'Energy']) | ||
columns=['SimId', 'AgentId', 'Year', 'Energy']) | ||
el_index = ['SimId', 'AgentId', 'Year'] | ||
elec = elec.groupby(el_index).sum() | ||
rtn = elec.reset_index() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,20 +2,30 @@ | |
generated by Cyclus itself. | ||
""" | ||
from __future__ import print_function, unicode_literals | ||
from cyclus import typesystem as ts | ||
|
||
from cymetric.evaluator import register_metric | ||
try: | ||
from cymetric.evaluator import register_metric | ||
from cymetric import schemas | ||
except ImportError: | ||
# some wacky CI paths prevent absolute importing, try relative | ||
from . import schemas | ||
from .evaluator import register_metric | ||
|
||
def _genrootclass(name): | ||
def _genrootclass(name, schema, register): | ||
"""Creates a new root metric class.""" | ||
if schema != None and not isinstance(schema, schemas.schema): | ||
schema = schemas.schema(schema) | ||
class Cls(object): | ||
dependencies = () | ||
|
||
@property | ||
def schema(self): | ||
"""Defines schema for root metric if provided.""" | ||
if self._schema is not None: | ||
return self._schema | ||
# fill in schema code | ||
registry = register | ||
#@property | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete these lines? |
||
#def schema(self): | ||
# """Defines schema for root metric if provided.""" | ||
# if self._schema is not None: | ||
# return self._schema | ||
# # fill in schema code | ||
|
||
@property | ||
def name(self): | ||
|
@@ -33,22 +43,40 @@ def __call__(self, conds=None, *args, **kwargs): | |
return None | ||
return self.db.query(self.name, conds=conds) | ||
|
||
Cls.schema = schema | ||
Cls.__name__ = str(name) | ||
register_metric(Cls) | ||
return Cls | ||
|
||
|
||
def root_metric(obj=None, name=None, schema=None, *args, **kwargs): | ||
def root_metric(obj=None, name=None, schema=None, registry=NotImplemented, *args, **kwargs): | ||
"""Decorator that creates a root metric from a function or class.""" | ||
if obj is not None: | ||
raise RuntimeError | ||
if name is None: | ||
raise RuntimeError | ||
return _genrootclass(name=name) | ||
return _genrootclass(name=name, schema=schema, register=registry) | ||
|
||
|
||
#core tables | ||
resources = root_metric(name='Resources') | ||
_resour_registry = { "Quantity": ["Units", "kg"]} | ||
_resource_shema = [ | ||
('SimId', ts.UUID), | ||
('ResourceId', ts.INT), | ||
('ObjId', ts.INT), | ||
('Type', ts.STRING), | ||
('TimeCreated', ts.INT), | ||
('Quantity', ts.DOUBLE), | ||
('Units', ts.STRING), | ||
('QualId', ts.INT), | ||
('Parent1', ts.INT), | ||
('Parent2', ts.INT) | ||
] | ||
resources = root_metric(name='Resources', schema=_resource_shema, registry=_resour_registry) | ||
|
||
#del _resour_registry, _resource_shema | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove commented lines |
||
#resources = root_metric(name='Resources') | ||
|
||
compositions = root_metric(name='Compositions') | ||
recipes = root_metric(name='Recipes') | ||
products = root_metric(name='Products') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
""" Convert able to the default unit system. | ||
""" | ||
import inspect | ||
|
||
try: | ||
from cymetric import schemas | ||
from cymetric import tools | ||
from cymetric import evaluator | ||
|
||
except ImportError: | ||
# some wacky CI paths prevent absolute importing, try relative | ||
from . import schemas | ||
from . import tools | ||
from . import evaluator | ||
|
||
import pint | ||
|
||
ureg = pint.UnitRegistry() | ||
|
||
class NormMetric(object): | ||
"""Metric class""" | ||
dependencies = NotImplemented | ||
schema = NotImplemented | ||
registry = NotImplemented | ||
|
||
def __init__(self, db): | ||
self.db = db | ||
|
||
@property | ||
def name(self): | ||
return self.__class__.__name__ | ||
|
||
def _gen_norm_metricclass(f, name, r_name, r_regitry, depends, scheme): | ||
"""Creates a new metric class with a given name, dependencies, and schema. | ||
|
||
Parameters | ||
---------- | ||
name : str | ||
Metric name | ||
depends : list of lists (table name, tuple of indices, column name) | ||
Dependencies on other database tables (metrics or root metrics) | ||
scheme : list of tuples (column name, data type) | ||
Schema for metric | ||
""" | ||
if not isinstance(scheme, schemas.schema): | ||
scheme = schemas.schema(scheme) | ||
|
||
class Cls(NormMetric): | ||
dependencies = depends | ||
schema = scheme | ||
func = staticmethod(f) | ||
raw_name = r_name | ||
raw_unit_registry = r_regitry | ||
__doc__ = inspect.getdoc(f) | ||
|
||
def __init__(self, db): | ||
"""Constructor for metric object in database.""" | ||
super(Cls, self).__init__(db) | ||
|
||
def __call__(self, frames, conds=None, known_tables=None, *args, **kwargs): | ||
"""Computes metric for given input data and conditions.""" | ||
# FIXME test if I already exist in the db, read in if I do | ||
if known_tables is None: | ||
known_tables = self.db.tables() | ||
if self.name in known_tables: | ||
return self.db.query(self.name, conds=conds) | ||
return f(self.raw_name, self.raw_unit_registry, *frames) | ||
|
||
Cls.__name__ = str(name) | ||
evaluator.register_metric(Cls) | ||
return Cls | ||
|
||
|
||
|
||
def norm_metric(name=None, raw_name=NotImplemented, raw_unit_registry=NotImplemented, depends=NotImplemented, schema=NotImplemented): | ||
"""Decorator that creates metric class from a function or class.""" | ||
def dec(f): | ||
clsname = name or f.__name__ | ||
return _gen_norm_metricclass(f=f, name=clsname, r_name=raw_name, r_regitry=raw_unit_registry, scheme=schema, depends=depends) | ||
return dec | ||
|
||
|
||
def build_conversion_col(col): | ||
conversion_col = [ureg.parse_expression( | ||
x).to_root_units().magnitude for x in col] | ||
default_unit = ureg.parse_expression(col[0]).to_root_units().units | ||
return conversion_col, default_unit | ||
|
||
|
||
def build_normalized_schema(raw_cls, unit_registry): | ||
if raw_cls.schema is None: | ||
return None | ||
# initialize the normed metric schema | ||
norm_schema = raw_cls.schema | ||
# removing units columns form the new schema | ||
for key in unit_registry: | ||
idx = norm_schema.index( (unit_registry[key][0], 4, None)) | ||
norm_schema.pop(idx) | ||
return norm_schema | ||
|
||
|
||
def build_normalized_metric(raw_metric): | ||
|
||
_norm_deps = [raw_metric.__name__] | ||
|
||
_norm_schema = build_normalized_schema(raw_metric, raw_metric.registry) | ||
_norm_name = "norm_" + raw_metric.__name__ | ||
_raw_name = raw_metric.__name__ | ||
_raw_units_registry = raw_metric.registry | ||
|
||
@norm_metric(name=_norm_name, raw_name=_raw_name, raw_unit_registry=_raw_units_registry, depends=_norm_deps, schema=_norm_schema) | ||
def new_norm_metric(raw_name, unit_registry, raw): | ||
|
||
norm_pdf = raw.copy(deep=True) | ||
for unit in unit_registry: | ||
u_col_name = unit_registry[unit][0] | ||
u_def_unit = unit_registry[unit][1] | ||
def_unit = "" | ||
# if a column for unit exist parse the colunm convert the value | ||
# drop the column | ||
if ( u_col_name != ""): | ||
conv, def_unit = build_conversion_col(raw[u_col_name]) | ||
norm_pdf[unit] *= conv | ||
norm_pdf.drop([u_col_name], axis=1, inplace=True) | ||
else: # else use the default unit to convert it | ||
conv = ureg.parse_expression(u_def_unit).to_root_units().magnitude | ||
def_unit = ureg.parse_expression(u_def_unit).to_root_units().units | ||
norm_pdf[unit] *= conv | ||
norm_pdf.rename(inplace=True, columns={unit : '{0} [{1:~P}]'.format(unit, def_unit)}) | ||
|
||
return norm_pdf | ||
|
||
del _norm_deps, _norm_schema, _norm_name | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we define a variable to be "norm_" to help guarantee consistency?