"""Base classes for bond graph models and connections."""
import logging
from collections import namedtuple
logger = logging.getLogger(__name__)
[docs]class BondGraphBase(object):
"""
Base class definition for all bond graphs.
Args:
name: Name of this model, assumed to be unique.
parent: Parent model, or none.
metamodel: Metamodel class.
"""
def __init__(self,
name=None,
parent=None,
metamodel=None,
**kwargs):
# TODO: This is a dirty hack
# Job for meta classes maybe?
if not metamodel:
self.__metamodel = "BG"
else:
self.__metamodel = metamodel
if not name:
self.name = f"{self.metamodel}" \
f"{self.__class__.instances}"
else:
self.name = name
self.parent = parent
"""Model that contains this object."""
def __repr__(self):
return f"{self.metamodel}: {self.name}"
def __new__(cls, *args, **kwargs): # noqa: D102
if "instances" not in cls.__dict__:
cls.instances = 1
else:
cls.instances += 1
return object.__new__(cls)
def __del__(self):
self.instances -= 1
@property
def metamodel(self):
"""The meta-model type of this object.""" # noqa: D401
return self.__metamodel
@property
def template(self):
"""The model template from which this was created.""" # noqa: D401
raise NotImplementedError
@property
def constitutive_relations(self):
"""The equations governing this objects dynamics.""" # noqa: D401
raise NotImplementedError
@property
def uri(self):
"""Model reference locator.""" # noqa: D401
if not self.parent:
return f"{self.name}:"
else:
return f"{self.parent.uri}/{self.name}"
@property
def root(self):
"""The root of the tree to which this object belongs.""" # noqa: D401
if not self.parent:
return self
else:
return self.parent.root
@property
def basis_vectors(self):
"""The input/ouput, dynamic and interconnect vectors.""" # noqa: D401
raise NotImplementedError
def __hash__(self):
return id(self)
[docs]class Bond(namedtuple("Bond", ["tail", "head"])):
"""Stores the connection between two ports.
Head and tail are specified to determine orientation
Attributes:
head: The 'harpoon' end of the power bond and direction of positive $f$
tail: The non-harpoon end, and direction of negative $f$
"""
def __contains__(self, item):
if isinstance(item, BondGraphBase):
return self.head[0] is item or self.tail[0] is item
try:
c, i = item
return any(c == comp and i == idx for comp, idx in self)
except TypeError:
return False
[docs]class Port(object):
"""Basic object for representing ports.
Looks and behaves like a namedtuple:
component, index = Port
Args:
component: The owner of this port.
index: The index of this port in the component.
"""
def __init__(self, component, index):
self.component = component
"""(`PortManager`) The component that this port is attached to"""
self.index = index
"""(int) The numerical index of this port"""
self.is_connected = False
"""(bool) True if this port is plugged in."""
def __iter__(self):
return iter((self.component, self.index))
def __len__(self):
return 2
def __getitem__(self, item):
if item == 0:
return self.component
elif item == 1:
return self.index
else:
raise KeyError
def __contains__(self, item):
return item is self.component
def __hash__(self):
return id(self)
def __str__(self):
if len(self.component.ports) > 1:
return f"{self.component.name}.{self.index}"
else:
return f"{self.component.name}"
def __repr__(self):
return f"Port({self.component}, {self.index})"
def __eq__(self, other):
try:
return ((self.component is other.component) and # noqa
(self.index == other.index))
except AttributeError:
try:
c, p = other
return c is self.component and p == self.index
except (AttributeError, TypeError):
pass
return False