Skip to content

Commit

Permalink
Creating base pydantic model and updating nodes.py to extend. Include…
Browse files Browse the repository at this point in the history
…s CIM mapping
  • Loading branch information
tareknrel committed Dec 6, 2023
1 parent 2e4672a commit a63e1f6
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 245 deletions.
171 changes: 44 additions & 127 deletions ditto/models/base.py
Original file line number Diff line number Diff line change
@@ -1,137 +1,54 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from builtins import super, range, zip, round, map
from pydantic import BaseModel, Field, ValidationError
from pydantic.json import isoformat, timedelta_isoformat

import warnings
from traitlets.traitlets import (
_CallbackWrapper,
EventHandler,
)
import traitlets as T

import logging

logger = logging.getLogger(__name__)


class DiTToHasTraits(T.HasTraits):

response = T.Any(allow_none=True, help="default trait for managing return values")

def __init__(self, model, *args, **kwargs):
model.model_store.append(self)
self.build(model)
super().__init__(*args, **kwargs)

def set_name(self, model):
try:
name = self.name
if name in model.model_names:
warnings.warn("Duplicate name %s being set. Object overwritten." % name)
logger.debug("Duplicate name %s being set. Object overwritten." % name)
logger.debug(model.model_names[name], self)
model.model_names[name] = self
except AttributeError:
pass

def build(self, model):
raise NotImplementedError(
"Build function must be implemented by derived classes"
)

def notify_access(self, bunch):
if not isinstance(bunch, T.Bunch):
# cast to bunch if given a dict
bunch = T.Bunch(bunch)
name, type = bunch.name, bunch.type

callables = []
callables.extend(self._trait_notifiers.get(name, {}).get(type, []))
callables.extend(self._trait_notifiers.get(name, {}).get(T.All, []))
callables.extend(self._trait_notifiers.get(T.All, {}).get(type, []))
callables.extend(self._trait_notifiers.get(T.All, {}).get(T.All, []))

# Call them all now
# Traits catches and logs errors here. I allow them to raise
if len(callables) > 1:
raise TypeError(
"Maximum number of callables allowed for a single attribute using the 'fetch' event is 1. Please check the documentation of DiTTo"
)
for c in callables:
# Bound methods have an additional 'self' argument.

if isinstance(c, _CallbackWrapper):
c = c.__call__
elif isinstance(c, EventHandler) and c.name is not None:
c = getattr(self, c.name)

return c(bunch)


class DiTToTraitType(T.TraitType):

allow_none = True

def get(self, obj, cls=None):
# Call notify_access with event type fetch
# If and only if one event exists, a return value will be produced
# This return value is saved as the current value in obj._trait_values
# Then call super get
try:
r = obj.notify_access(
T.Bunch(
name=self.name,
value=obj._trait_values[self.name],
owner=self,
type="fetch",
)
)

old_value = obj._trait_values[self.name]
from .units import Voltage, VoltageUnit, Phase
from .position import Position

if r is not None and r != old_value:
logger.debug(
"Response from callback event 'fetch' on property {} does not match previous value. Overloading existing value {} with new value {}".format(
self.name, old_value, r
)
)
obj._trait_values[self.name] = r
except KeyError:
pass

return super().get(obj, cls=cls)


class Float(T.Float, DiTToTraitType):
pass


class Complex(T.Complex, DiTToTraitType):
pass


class Unicode(T.Unicode, DiTToTraitType):
pass


class Any(T.Any, DiTToTraitType):
pass


class Int(T.Int, DiTToTraitType):
pass


class List(T.List, DiTToTraitType):
pass


class Instance(T.Instance, DiTToTraitType):
pass


class Bool(T.Bool, DiTToTraitType):
pass
logger = logging.getLogger(__name__)


observe = T.observe
class DiTToBaseModel(BaseModel):
""" Base pydantic class for all DiTTo models.
A name is required for all DiTTo models.
"""

class Config:
""" Base pydantic configuration for all DiTTo models.
"""
title = "DiTToBaseModel"
validate_assignment = True
validate_all = True
extra = "forbid"
use_enum_values = True
arbitrary_types_allowed = True
allow_population_by_field_name = True

name: str = Field(
description="Name of the element in the DiTTo model",
title="name",
cim_value="name"
)

substation_name: Optional[str] = Field(
description="Name of the substation the element is under",
title="substation_name"
cim_value="EquipmentContainer.Substation.name"
)

feeder_name: Optional[str] = Field(
description="Name of the feeder the element is on",
title="feeder_name"
cim_value="EquipmentContainer.name"
)

positions: Optional[List[Position]] = Field(
description="A list of positions of the element. For single point elements, this list should have a length of one. For lines, this list may contain intermediate points.",
title="positions"
cim_value="Location.PositionPoints"
)
132 changes: 21 additions & 111 deletions ditto/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,124 +2,34 @@
from __future__ import absolute_import, division, print_function
from builtins import super, range, zip, round, map

from .base import (
DiTToHasTraits,
Float,
Unicode,
Any,
Bool,
Int,
List,
observe,
Instance,
)

from .base import DiTToBaseModel
from .units import Voltage, VoltageUnit, Phase
from .position import Position


class Node(DiTToHasTraits):
"""Inheritance:
Asset (self._asset)
-> Location (self._loc)
ConnectivityNode (self._cn)
"""

name = Unicode(help="""Name of the node object""")
nominal_voltage = Float(
help="""This parameter defines the base voltage at the node.""",
default_value=None,

nominal_voltage: Optional[Voltage] = Field(
description = "The nominal voltage at the node",
title = "Nominal voltage",
cim_value="nomU"
)
phases = List(
Instance(Unicode),
help="""This parameter is a list of all the phases at the node.""",
)
positions = List(
Instance(Position),
help="""This parameter is a list of positional points describing the node - it should only contain one.
The positions are objects containing elements of long, lat and elevation.""",
)

# Modification: Nicolas (December 2017)
# Multiple feeder support. Each element keeps track of the name of the substation it is connected to, as well as the name of the feeder.
# I think we need both since a substation might have multiple feeders attached to it.
# These attributes are filled once the DiTTo model has been created using the Network module
substation_name = Unicode(
help="""The name of the substation to which the object is connected.""",
).tag(default=None)
feeder_name = Unicode(
help="""The name of the feeder the object is on.""",
).tag(default=None)

# Modification: Tarek (April 2018)
# Support for substation connection points. These identify if the node connects the substation to a feeder or high voltage source
is_substation_connection = Bool(
help="""1 if the node connects from inside a substation to outside, 0 otherwise.""",
).tag(default=None)

# Modification: Nicolas (May 2018)
is_substation = Bool(
help="""Flag that indicates wheter the element is inside a substation or not.""",
default_value=False,
phases: Optional[List[Phase]] = Field(
description="Phases at the node",
title="phases"
cim_value="phases"
)

setpoint = Float(
help="""Value that the node must be set to. This is typically used for feeder head points""",
default_value=None,
is_substation_connection: Optional[bool] = Field(
description="1 if the node connects from inside a substation to outside, 0 otherwise. These indicate if a node connects a substation to a feeder or high voltage source",
title="is_substation_connection",
default=False,
cim_value="NA"
)

def build(self, model, Asset=None, ConnectivityNode=None, Location=None):

self._model = model


# if ConnectivityNode is None:
# self._cn = self._model.env.ConnectivityNode()
# else:
# self._cn = ConnectivityNode
#
# if Asset is None:
# self._asset = self._model.env.Asset()
# else:
# self._asset = Asset
# self._asset.PowerSystemResource = self._asset.PowerSystemResource + (self._cn, )
#
# if Location is None:
# self._loc = self._model.env.Location()
# else:
# self._loc = Location
# self._asset.Location = self._loc
# self._loc.Assets = self._loc.Assets + (self._asset, )
#
# self._model.model_store[self.name] = self
#
# @observe('name', type='change')
# def _set_name(self, bunch):
# self._cn.name = bunch['new']
#
# @observe('name', type='fetch')
# def _get_name(self, bunch):
# return self._cn.name
#
# @observe('positions', type='change')
# def _set_positions(self, bunch):
# position_list = bunch['new']
# self._loc.PositionPoints=[]
# for position in position_list:
# p = self._model.env.PositionPoint()
# p.xPosition = position.long
# p.yPosition = position.lat
# p.zPosition = position.elevation
# p.Location = self._loc
# self._loc.PositionPoints = self._loc.PositionPoints + (p, )
#
# @observe('positions', type='fetch')
# def _get_positions(self, bunch):
# positions = []
# for p in self._loc.PositionPoints:
# position = Position()
# position.lat = p.xPosition
# position.long = p.yPosition
# position.elevation = p.zPosition
# positions.append(position)
# return positions
setpoint: Optional[Voltage] = Field(
description="Value that the node must be set to. This is typically used for feeder head points",
title="setpoint",
cim_value="NA"
)
41 changes: 41 additions & 0 deletions ditto/models/units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from builtins import super, range, zip, round, map
from pydantic import BaseModel, Field, ValidationError
from pydantic.json import isoformat, timedelta_isoformat

import warnings
import logging

logger = logging.getLogger(__name__)

class Voltage(BaseModel):
"""This class is used to represent the voltage at a node or a line. It is a simple container for the voltage value and the unit.
"""
value: float = Field(
title="value",
description="The value of the voltage",
)
unit: VoltageUnit = Field(
description="The unit of the voltage",
title="unit",
default="V",
)

class VoltageUnit(str, Enum):
"""This class is used to represent the possible units of voltage.
"""
V = "V"
kV = "kV"
MV = "MV"


class Phase(str, Enum):
"""This class is used to represent a single phase from a set of possible values.
"""
A = "A"
B = "B"
C = "C"
N = "N"
s1 = "s1"
s2 = "s2"
Loading

0 comments on commit a63e1f6

Please sign in to comment.