Skip to content
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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
28 changes: 20 additions & 8 deletions cymetric/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Member

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?

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we force this to False here? Maybe a comment?

frames.append(frame)
raw = m(frames=frames, conds=conds, known_tables=self.known_tables)
if raw is None:
Expand All @@ -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)



21 changes: 14 additions & 7 deletions cymetric/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Metric(object):
"""Metric class"""
dependencies = NotImplemented
schema = NotImplemented
registry = NotImplemented

def __init__(self, db):
self.db = db
Expand All @@ -40,7 +41,7 @@ def name(self):
return self.__class__.__name__


def _genmetricclass(f, name, depends, scheme):
def _genmetricclass(f, name, depends, scheme, register):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update docstring to describe register

"""Creates a new metric class with a given name, dependencies, and schema.

Parameters
Expand All @@ -59,8 +60,11 @@ class Cls(Metric):
dependencies = depends
schema = scheme
func = staticmethod(f)

registry = register
__doc__ = inspect.getdoc(f)

def shema(self):
Copy link
Member

Choose a reason for hiding this comment

The 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 self.schema?

return schema

def __init__(self, db):
"""Constructor for metric object in database."""
Expand All @@ -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


Expand All @@ -105,8 +110,9 @@ def dec(f):
('Units', ts.STRING),
('Mass', ts.DOUBLE)
]
_matregistry = { "Mass": ["Units", "kg"]}
Copy link
Member

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
50 changes: 39 additions & 11 deletions cymetric/root_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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):
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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')
Expand Down
135 changes: 135 additions & 0 deletions cymetric/units.py
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