Files
deb-python-pint/pint/systems.py
2015-07-24 00:53:24 -03:00

424 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""
pint.systems
~~~~~~~~~~~~
Functions and classes related to system definitions and conversions.
:copyright: 2013 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import division, unicode_literals, print_function, absolute_import
import re
from .unit import Definition, UnitDefinition, DefinitionSyntaxError
from .util import to_units_container, UnitsContainer
class Group(object):
"""A group is a list of units.
Units can be added directly or by including other groups.
Members are computed dynamically, that is if a unit is added to a group X
all groups that include X are affected.
"""
#: Regex to match the header parts of a context.
_header_re = re.compile('@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*')
def __init__(self, name, groups_systems):
"""
:param name: Name of the group
:type name: str
:param groups_systems: dictionary containing groups and system.
The newly created group will be added after creation.
:type groups_systems: dict[str, Group | System]
"""
if name in groups_systems:
t = 'group' if isinstance(groups_systems['name'], Group) else 'system'
raise ValueError('The system name already in use by a %s' % t)
# The name of the group.
#: type: str
self.name = name
#: Names of the units in this group.
#: :type: set[str]
self._unit_names = set()
#: Names of the groups in this group.
#: :type: set[str]
self._used_groups = set()
#: Names of the groups in which this group is contained.
#: :type: set[str]
self._used_by = set()
#: Maps group name to Group.
#: :type: dict[str, Group]
self._groups_systems = groups_systems
self._groups_systems[self.name] = self
if name != 'root':
# All groups are added to root group
groups_systems['root'].add_groups(name)
#: A cache of the included units.
#: None indicates that the cache has been invalidated.
#: :type: frozenset[str] | None
self._computed_members = None
@property
def members(self):
"""Names of the units that are members of the group.
Calculated to include to all units in all included _used_groups.
:rtype: frozenset[str]
"""
if self._computed_members is None:
self._computed_members = set(self._unit_names)
for _, group in self.iter_used_groups():
self._computed_members |= group.members
self._computed_members = frozenset(self._computed_members)
return self._computed_members
def invalidate_members(self):
"""Invalidate computed members in this Group and all parent nodes.
"""
self._computed_members = None
d = self._groups_systems
for name in self._used_by:
d[name].invalidate_members()
def iter_used_groups(self):
pending = set(self._used_groups)
d = self._groups_systems
while pending:
name = pending.pop()
group = d[name]
pending |= group._used_groups
yield name, d[name]
def is_used_group(self, group_name):
for name, _ in self.iter_used_groups():
if name == group_name:
return True
return False
def add_units(self, *unit_names):
"""Add units to group.
:type unit_names: str
"""
for unit_name in unit_names:
self._unit_names.add(unit_name)
self.invalidate_members()
def remove_units(self, *unit_names):
"""Remove units from group.
:type unit_names: str
"""
for unit_name in unit_names:
self._unit_names.remove(unit_name)
self.invalidate_members()
def add_groups(self, *group_names):
"""Add groups to group.
:type group_names: str
"""
d = self._groups_systems
for group_name in group_names:
grp = d[group_name]
if grp.is_used_group(self.name):
raise ValueError('Cyclic relationship found between %s and %s' % (self.name, group_name))
self._used_groups.add(group_name)
grp._used_by.add(self.name)
self.invalidate_members()
def remove_groups(self, *group_names):
"""Remove groups from group.
:type group_names: str
"""
d = self._groups_systems
for group_name in group_names:
grp = d[group_name]
self._used_groups.remove(group_name)
grp._used_by.remove(self.name)
self.invalidate_members()
@classmethod
def from_lines(cls, lines, define_func, group_dict):
"""Return a Group object parsing an iterable of lines.
:param lines: iterable
:type lines: list[str]
:param define_func: Function to define a unit in the registry.
:type define_func: str -> None
:param group_dict: Maps group name to Group.
:type group_dict: dict[str, Group]
"""
header, lines = lines[0], lines[1:]
r = cls._header_re.search(header)
name = r.groupdict()['name'].strip()
groups = r.groupdict()['used_groups']
if groups:
group_names = tuple(a.strip() for a in groups.split(','))
else:
group_names = ()
unit_names = []
for line in lines:
if '=' in line:
# Is a definition
definition = Definition.from_string(line)
if not isinstance(definition, UnitDefinition):
raise DefinitionSyntaxError('Only UnitDefinition are valid inside _used_groups, '
'not %s' % type(definition))
define_func(definition)
unit_names.append(definition.name)
else:
unit_names.append(line.strip())
grp = cls(name, group_dict)
grp.add_units(*unit_names)
if group_names:
grp.add_groups(*group_names)
return grp
class System(object):
"""A system is a Group plus a set of base units.
@system <name> [using <group 1>, ..., <group N>]
<rule 1>
...
<rule N>
@end
"""
#: Regex to match the header parts of a context.
_header_re = re.compile('@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*')
def __init__(self, name, groups_systems):
"""
:param name: Name of the group
:type name: str
:param groups_systems: dictionary containing groups and system.
The newly created group will be added after creation.
:type groups_systems: dict[str, Group | System]
"""
if name in groups_systems:
t = 'group' if isinstance(groups_systems[name], Group) else 'system'
raise ValueError('The system name (%s) already in use by a %s' % (name, t))
#: Name of the system
#: :type: str
self.name = name
#: Maps root unit names to a dict indicating the new unit and its exponent.
#: :type: dict[str, dict[str, number]]]
self.base_units = {}
#: Derived unit names.
#: :type: set(str)
self.derived_units = set()
#: Names of the _used_groups in used by this system.
#: :type: set(str)
self._used_groups = set()
#: :type: frozenset | None
self._computed_members = None
#: Maps group name to Group.
self._group_systems_dict = groups_systems
self._group_systems_dict[self.name] = self
@property
def members(self):
if self._computed_members is None:
self._computed_members = set()
for group_name in self._used_groups:
self._computed_members |= self._group_systems_dict[group_name].members
self._computed_members = frozenset(self._computed_members)
return self._computed_members
def invalidate_members(self):
"""Invalidate computed members in this Group and all parent nodes.
"""
self._computed_members = None
def add_groups(self, *group_names):
"""Add groups to group.
:type group_names: str
"""
self._used_groups |= set(group_names)
self.invalidate_members()
def remove_groups(self, *group_names):
"""Remove groups from group.
:type group_names: str
"""
self._used_groups -= set(group_names)
self.invalidate_members()
@classmethod
def from_lines(cls, lines, get_root_func, group_dict):
header, lines = lines[0], lines[1:]
r = cls._header_re.search(header)
name = r.groupdict()['name'].strip()
groups = r.groupdict()['used_groups']
# If the systems has no group, it automatically uses the root group.
if groups:
group_names = tuple(a.strip() for a in groups.split(','))
else:
group_names = ('root', )
base_unit_names = {}
derived_unit_names = []
for line in lines:
line = line.strip()
# We would identify a
# - old_unit: a root unit part which is going to be removed from the system.
# - new_unit: a non root unit which is going to replace the old_unit.
if ':' in line:
# The syntax is new_unit:old_unit
new_unit, old_unit = line.split(':')
new_unit, old_unit = new_unit.strip(), old_unit.strip()
# The old unit MUST be a root unit, if not raise an error.
if old_unit != str(get_root_func(old_unit)[1]):
raise ValueError('In `%s`, the unit at the right of the `:` must be a root unit.' % line)
# Here we find new_unit expanded in terms of root_units
new_unit_expanded = to_units_container(get_root_func(new_unit)[1])
# We require that the old unit is present in the new_unit expanded
if old_unit not in new_unit_expanded:
raise ValueError('Old unit must be a component of new unit')
# Here we invert the equation, in other words
# we write old units in terms new unit and expansion
new_unit_dict = dict((new_unit, -1./value)
for new_unit, value in new_unit_expanded.items()
if new_unit != old_unit)
new_unit_dict[new_unit] = 1 / new_unit_expanded[old_unit]
base_unit_names[old_unit] = new_unit_dict
else:
# The syntax is new_unit
# old_unit is inferred as the root unit with the same dimensionality.
new_unit = line
old_unit_dict = to_units_container(get_root_func(line)[1])
if len(old_unit_dict) != 1:
raise ValueError('The new base must be a root dimension if not discarded unit is specified.')
old_unit, value = dict(old_unit_dict).popitem()
base_unit_names[old_unit] = {new_unit: 1./value}
system = cls(name, group_dict)
system.add_groups(*group_names)
system.base_units.update(**base_unit_names)
system.derived_units |= set(derived_unit_names)
return system
class GSManager(object):
def __init__(self):
#: :type: dict[str, Group | System]
self._groups_systems = dict()
self._root_group = Group('root', self._groups_systems)
def add_system_from_lines(self, lines, get_root_func):
System.from_lines(lines, get_root_func, self._groups_systems)
def add_group_from_lines(self, lines, define_func):
Group.from_lines(lines, define_func, self._groups_systems)
def get_group(self, name, create_if_needed=True):
"""Return a Group.
:param name: Name of the group.
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: Group
"""
try:
return self._groups_systems[name]
except KeyError:
if create_if_needed:
if name == 'root':
raise ValueError('The name root is reserved.')
return Group(name, self._groups_systems)
else:
raise KeyError('No group %s found.' % name)
def get_system(self, name, create_if_needed=True):
"""Return a Group.
:param name: Name of the system
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: System
"""
try:
return self._groups_systems[name]
except KeyError:
if create_if_needed:
return System(name, self._groups_systems)
else:
raise KeyError('No system found named %s.' % name)
def __getitem__(self, item):
if item in self._groups_systems:
return self._groups_systems[item]
raise KeyError('No group or system found named %s.' % item)