Source code for chemdataextractor.model.units.quantity_model

# -*- coding: utf-8 -*-
"""
Base types for making quantity models.

:codeauthor: Taketomo Isazawa ([email protected])
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import six
import copy
from abc import ABCMeta
from ..base import BaseModel, BaseType, FloatType, StringType, ListType, ModelMeta, InferredProperty
from .unit import Unit, UnitType
from .dimension import Dimensionless
from ...parse.elements import Any
from ...parse.auto import AutoSentenceParser, AutoTableParser, construct_unit_element, match_dimensions_of
from ...parse.quantity import magnitudes_dict, infer_unit, infer_value, infer_error, value_element
from ...parse.template import QuantityModelTemplateParser, MultiQuantityModelTemplateParser


class _QuantityModelMeta(ModelMeta):
    """"""

    def __new__(mcs, name, bases, attrs):
        cls = super(_QuantityModelMeta, mcs).__new__(mcs, name, bases, attrs)
        unit_element = construct_unit_element(cls.dimensions)
        if unit_element:
            cls.fields['raw_units'].parse_expression = unit_element(None)
        cls.fields['raw_value'].parse_expression = value_element()(None)
        return cls


[docs]class QuantityModel(six.with_metaclass(_QuantityModelMeta, BaseModel)): """ Class for modelling quantities. Subclasses of this model can be used in conjunction with Autoparsers to extract properties with zero human intervention. However, they must be constructed in a certain way for them to work optimally with autoparsers. Namely, they should have: - A specifier field with an associated parse expression (Optional, only required if autoparsers are desired). These parse expressions will be updated automatically using forward-looking Interdependency Resolution if the updatable flag is set to True. - These specifiers should also have required set to True so that spurious matches are not found. - If applicable, a compound field, named compound. Any parse_expressions set in the model should have an added action to ensure that the results are a single word. An example would be to call add_action(join) on each parse expression. """ raw_value = StringType(required=True, contextual=True) raw_units = StringType(required=True, contextual=True) value = InferredProperty(ListType(FloatType(), sorted_=True), origin_field='raw_value', inferrer=infer_value, contextual=True) units = InferredProperty(UnitType(), origin_field='raw_units', inferrer=infer_unit, contextual=True) error = InferredProperty(FloatType(), origin_field='raw_value', inferrer=infer_error, contextual=True) dimensions = None specifier = StringType() parsers = [MultiQuantityModelTemplateParser(), QuantityModelTemplateParser(), AutoTableParser()] # Operators are implemented so that composite quantities can be created easily # on the fly, such as the following code snippet: # .. code-block:: python # length = LengthModel() # length.unit = Meter() # length.value = 10 # time = TimeModel() # time.unit = Second() # time.value = [2] # speed = length / time # print("Speed in miles per hour is: ", speed.convert_to(Mile() / Hour()).value[0]) # Which has an expected output of # Speed in miles per hour is: 11.184709259696522 def __truediv__(self, other): other_inverted = other**(-1.0) new_model = self * other_inverted return new_model def __pow__(self, other): new_model = QuantityModel() new_model.dimensions = self.dimensions ** other if self.value is not None: new_val = [] for val in self.value: new_val.append(val ** other) new_model.value = new_val if self.units is not None: new_model.units = self.units ** other # Handle case that we have a dimensionless quantity, so we don't get dimensionless units squared. if isinstance(new_model.dimensions, Dimensionless): dimensionless_model = DimensionlessModel() dimensionless_model.value = new_model.value dimensionless_model.units = new_model.units return dimensionless_model return new_model def __mul__(self, other): new_model = QuantityModel() new_model.dimensions = self.dimensions * other.dimensions if self.value is not None and other.value is not None: if len(self.value) == 2 and len(other.value) == 2: # The following always encompasses the whole range because # value has sorted_=True, so it should sort any values. new_val = [self.value[0] * other.value[0], self.value[1] * other.value[1]] new_model.value = new_val elif len(self.value) == 2: new_val = [self.value[0] * other.value[0], self.value[1] * other.value[0]] new_model.value = new_val elif len(self.value) == 2 and len(other.value) == 2: new_val = [self.value[0] * other.value[0], self.value[0] * other.value[1]] new_model.value = new_val else: new_model.value = [self.value[0] * other.value[0]] if self.units is not None and other.units is not None: new_model.units = self.units * other.units if isinstance(new_model.dimensions, Dimensionless): dimensionless_model = DimensionlessModel() dimensionless_model.value = new_model.value dimensionless_model.units = new_model.units return dimensionless_model return new_model
[docs] def convert_to(self, unit): """ Convert from current units to the given units. Raises AttributeError if the current unit is not set. .. note:: This method both modifies the current model and returns the modified model. :param Unit unit: The Unit to convert to :returns: The quantity in the given units. :rtype: QuantityModel """ if self.units: try: converted_values = self.convert_value(self.units, unit) if self.error: converted_error = self.convert_error(self.units, unit) self.error = converted_error self.value = converted_values self.units = unit except ZeroDivisionError: raise ValueError('Model not converted due to zero division error') else: raise AttributeError('Current units not set') return self
[docs] def convert_to_standard(self): """ Convert from current units to the standard units. Raises AttributeError if the current unit has not been set or the dimensions do not have standard units. .. note:: This method both modifies the current model and returns the modified model. :returns: The quantity in the given units. :rtype: QuantityModel """ standard_units = self.dimensions.standard_units if self.units and standard_units is not None: self.convert_to(standard_units) else: if not self.units: raise AttributeError('Current units not set') elif not self.dimensions.standard_units: raise AttributeError('Standard units for dimension', self.dimension, 'not set') return self
[docs] def convert_value(self, from_unit, to_unit): """ Convert between the given units. If no units have been set for this model, assumes that it's in standard units. :param Unit from_unit: The Unit to convert from :param Unit to_unit: The Unit to convert to :returns: The value as expressed in the new unit :rtype: float """ if self.value is not None: if to_unit.dimensions == from_unit.dimensions: if len(self.value) == 2: standard_vals = [from_unit.convert_value_to_standard(self.value[0]), from_unit.convert_value_to_standard(self.value[1])] return [to_unit.convert_value_from_standard(standard_vals[0]), to_unit.convert_value_from_standard(standard_vals[1])] else: standard_val = from_unit.convert_value_to_standard(self.value[0]) return [to_unit.convert_value_from_standard(standard_val)] else: raise ValueError("Unit to convert to must have same dimensions as current unit") raise AttributeError("Unit to convert from not set") else: raise AttributeError("Value for model not set")
[docs] def convert_error(self, from_unit, to_unit): """ Converts error between given units If no units have been set for this model, assumes that it's in standard units. :param Unit from_unit: The Unit to convert from :param Unit to_unit: The Unit to convert to :returns: The error as expressed in the new unit :rtype: float """ if self.error is not None: if to_unit.dimensions == from_unit.dimensions: standard_error = from_unit.convert_error_to_standard(self.error) return to_unit.convert_error_from_standard(standard_error) else: raise ValueError("Unit to convert to must have same dimensions as current unit") raise AttributeError("Unit to convert from not set") else: raise AttributeError("Value for model not set")
[docs] def is_equal(self, other): """ Tests whether the two quantities are physically equal, i.e. whether they represent the same value just in different units. :param QuantityModel other: The quantity being compared with :returns: Whether the two quantities are equal :rtype: bool """ if self.value is None or other.value is None: raise AttributeError("Value for model not set") if self.units is None or other.units is None: raise AttributeError("Units not set") converted_value = self.convert_value(self.units, other.units) min_converted_value = converted_value[0] max_converted_value = converted_value[0] if len(converted_value) == 2: max_converted_value = converted_value[1] if self.error is not None: converted_error = self.convert_error(self.units, other.units) min_converted_value = min_converted_value - converted_error max_converted_value = max_converted_value + converted_error min_other_value = other.value[0] max_other_value = other.value[0] if len(other.value) == 2: max_other_value = other.value[1] if other.error is not None: min_other_value = min_other_value - other.error max_other_value = max_other_value + other.error if min_converted_value <= max_other_value or max_converted_value >= min_other_value: return True return False
[docs] def is_superset(self, other): if type(self) != type(other): return False for field_name, field in six.iteritems(self.fields): # Method works recursively so it works with nested models if hasattr(field, 'model_class'): if self[field_name] is None: if other[field_name] is not None: return False elif other[field_name] is None: pass elif not self[field_name].is_superset(other[field_name]): return False else: if (field_name == 'raw_value' and other[field_name] == 'NoValue' and self[field_name] is not None): pass elif other[field_name] is not None and self[field_name] != other[field_name]: return False return True
def _compatible(self, other): match = False if type(other) == type(self): # Check if the other seems to be describing the same thing as self. match = True for field_name, field in six.iteritems(self.fields): if (field_name == 'raw_value' and other[field_name] == 'NoValue' and self[field_name] is not None): pass elif (self[field_name] is not None and other[field_name] is not None and self[field_name] != other[field_name]): match = False break return match def __str__(self): string = 'Quantity with ' + self.dimensions.__str__() + ', ' + self.units.__str__() string += ' and a value of ' + str(self.value) return string
[docs]class DimensionlessModel(QuantityModel): """ Special case to handle dimensionless quantities""" dimensions = Dimensionless() raw_units = StringType(required=False, contextual=False)