Skip to main content
Version: Next

CIM Services

Now that you are able to create a data model, you need something to manage the objects. This is where we provide a set of classes, which we call services, to do this.

The services basically act as a container for your model. However, they provide some features which make it much nicer to use than a typical map / dictionary type data container to store your identified objects. The SDK provides the following services

  • NetworkService
  • CustomerService
  • DiagramService
  • MeasurementService

Each of the first three services manage a specific subset of identified objects. This subset will hopefully be somewhat obvious based on the name of each service. MeasurementService is the odd one out: it stores MeasurementValue objects, which are the values reported against Measurement definitions, rather than IdentifiedObjects. If you would like to know why there are multiple services for different object types you can read more about it here.

Common Service functionality​

Because services work with identified objects they can provide some common level of functionality. This functionality can be found in the base class BaseService. MeasurementService is the exception: it does not extend BaseService because it stores MeasurementValues rather than identified objects, so the methods described in this section do not apply to it.

Adding and Removing Objects​

The BaseService.add method can be used to add objects to the service. For each service type there is a set of supported classes, and you should only add objects of those types to the service. Objects are disassociated again with BaseService.remove, which will also drop any indexing the specialised service has built up for that object. Note that in some cases where objects are indexed differently other add style methods will be present, which should be utilised to allow efficient querying of the types they support (e.g add_diagram_object in DiagramService, or add_measurement in NetworkService).

from zepben.ewb import NetworkService, Diagram, Breaker

service = NetworkService()

breaker = Breaker(mrid="breaker")
service.add(breaker)

# Note that you should only add types intended for the corresponding service. A Diagram is managed by the DiagramService
# (see below), so adding it to the NetworkService is incorrect even though add will accept it:
diagram = Diagram(mrid="diagram")
service.add(diagram) # Wrong service.

Object Retrieval​

There are a few ways we provide to get objects back out of a service. The most obvious one is by mRID:

from zepben.ewb import NetworkService, Breaker

service = NetworkService()
service.add(Breaker("breaker1"))
breaker = service.get("breaker1")

# Qualifying the type yields a minor performance optimisation.
breaker = service.get("breaker1", Breaker)

Implementations of BaseService may have more dedicated methods which support more forms of retrieval.

You can also get collections of objects back out of the service. The power here is you can get objects from anywhere in the CIM class hierarchy. Due to the internal data structures used by the services, using these functions is more efficient than looping over all objects in the service and checking if they are of the required type.

from zepben.ewb import NetworkService, Breaker, Junction, ConductingEquipment

service = NetworkService()

service.add(Breaker(mrid="breaker"))
service.add(Junction(mrid="junction"))

# service.objects() can be used to get a generator over the objects in the service, and supports selecting by type.
for obj in service.objects(): # generator over all objects in service
pass # do stuff with obj

for obj in service.objects(ConductingEquipment): # generator over all objects in service subclassing ConductingEquipment
pass # do stuff with obj

for obj in service.objects(Breaker): # generator over all objects in service of type Breaker (in this case the one Breaker we added)
obj.set_open(True)

print(f"{service.len_of()} objects, {service.len_of(Breaker)} of which are Breakers")

Network Service​

The network service works with objects related to the physical asset model. That means all PowerSystemResource types, but also data related types such as AssetInfo and Location.

The network service also keeps an index of the measurements it holds. A Measurement is indexed when you add it with add_measurement, which records it under the measurement's own mRID; a plain add stores the measurement without indexing it. get_measurements then returns the measurements recorded under an mRID, filtered by the type you pass:

from zepben.ewb import NetworkService, Analog, Accumulator, Measurement

service = NetworkService()

amps = Analog(mrid="amps", power_system_resource_mrid="ASWITCH")
count = Accumulator(mrid="count", power_system_resource_mrid="ASWITCH")

# add_measurement stores the measurement and records it under its own mRID.
service.add_measurement(amps)
service.add_measurement(count)

# [Analog{amps}]
service.get_measurements("amps", Measurement)

# [Accumulator{count}]
service.get_measurements("count", Measurement)

# [Analog{amps}], asking for a narrower type only narrows the result
service.get_measurements("amps", Analog)

# []
service.get_measurements("amps", Accumulator)

The index key is the measurement's own mRID, so power_system_resource_mrid and terminal_mrid are not lookup keys, and changing them after the measurement has been added does not move the entry. get_measurements raises KeyError when nothing is recorded under the mRID you ask for, so get_measurements("ASWITCH", Measurement) only works when a measurement is itself named ASWITCH.

Customer Service​

The customer service works with objects related to customers and their agreements they may have with actors in the network. At this point in time it provides no further specialised functionality.

Diagram Service​

The diagram service works with objects related to diagrams associated with identified objects. It also provides a lookup to be able to get the DiagramObjects associated with any identified object. The lookup is meant to work like this; the note after the example says what works today:

  • If the mRID is a diagram object, a list with just the diagram object is returned.
  • If the mRID is a Diagram all diagram objects belonging to that diagram are returned.
  • If the mRID is any other identified object, the diagram objects for that identified object are returned.
from zepben.ewb import DiagramService, Diagram, DiagramObject

service = DiagramService()

a_diagram = Diagram(mrid="diagram")

# The diagram keyword argument sets the diagram reference on each diagram object. The diagram's own diagram_objects
# collection is only updated if you also call add_diagram_object on the diagram itself (see Diagram.add_diagram_object).
do1 = DiagramObject(mrid="do1", diagram=a_diagram, identified_object_mrid="aSwitch")
do2 = DiagramObject(mrid="do2", diagram=a_diagram, identified_object_mrid="aSwitch")

# add_diagram_object adds the object to the service and records it against its diagram and its identified object.
service.add_diagram_object(do1)
service.add_diagram_object(do2)

# Contains [DiagramObject{do1}]
objs = service.get_diagram_objects(do1.mrid)

Only the lookup by the diagram object's own mRID returns results today; a lookup by the diagram's mRID or by the represented object's mRID returns an empty list, or raises TypeError if the Diagram itself is in the service.

add_diagram_object records the diagram object against its Diagram and its identified_object_mrid when it is added, so set both before adding. identified_object_mrid can still be changed afterwards, but that does not update what was recorded, and a later remove then raises KeyError because it looks up the value currently set on the object. As a diagram object is unlikely to change the identified object it represents, setting it before adding avoids the problem.

Measurement Service​

The measurement service works with MeasurementValues - the values reported against Measurement definitions such as Analog, Accumulator and Discrete. The Measurement definitions themselves are managed by the network service (see Network Service), so keeping the values in their own service keeps the high volume, high churn value data separate from the asset model.

MeasurementService is the only service that does not extend BaseService, so it provides no lookup by mRID and no deferred reference handling. It provides add, remove, len_of and objects:

from datetime import datetime, timedelta

from zepben.ewb import MeasurementService, AnalogValue, AccumulatorValue

service = MeasurementService()

amps = AnalogValue(analog_mrid="amps", value=123.4, time_stamp=datetime.now() - timedelta(days=1))
count = AccumulatorValue(accumulator_mrid="count", value=42, time_stamp=datetime.now())

service.add(amps)
service.add(count)

# Generator over every measurement value in the service.
for value in service.objects():
print(f"{value.time_stamp}: {value}")

# len_of can count all values, or only values of a specific type.
print(service.len_of()) # 2
print(service.len_of(AnalogValue)) # 1

Deferred References​

When creating an object to include in the model, you will often not have all referenced objects constructed, not have all the information to construct a reference immediately, or not have a reference handy to use. However, you will generally have the mRID of any referenced objects. To deal with this, the services provide a resolve_or_defer_reference function. This function will:

  • Resolve a reference immediately (in both directions if applicable) if the reference mRID object is already added to the service.
  • If the reference mRID is not added to the service, the request to resolve the reference is cached. When an object with the referenced mRID is eventually added to the service, the reference is resolved at this time.

Unresolved references can also be queried back out of the service. Let's see it all with a simple example:

from zepben.ewb import NetworkService, Feeder, Breaker, Junction, resolver

service = NetworkService()

feeder = Feeder(mrid="f")
service.add(feeder)

switch = Breaker(mrid="b1")
service.add(switch)

# As the switch is already added to the service, this will be resolved immediately.
service.resolve_or_defer_reference(resolver.ec_equipment(feeder), switch.mrid)
print(switch in list(feeder.equipment)) # True

# Now if we try and resolve something not added it will be deferred
junction = Junction(mrid="j1")
service.resolve_or_defer_reference(resolver.ec_equipment(feeder), junction.mrid)
print(junction in list(feeder.equipment)) # False

# We can query the unresolved reference MRIDs back out of the service using a specific resolver
print(list(service.get_unresolved_reference_mrids_by_resolver(resolver.ec_equipment(feeder)))) # ["j1"]
# Or the full unresolved references, using the mRID of the destination object
print([ur.from_ref.mrid for ur in service.get_unresolved_references_to(junction.mrid)]) # ["f"]
# Or using the mRID of the source object
print([ur.to_mrid for ur in service.get_unresolved_references_from(feeder.mrid)]) # ["j1"]

# When the object with the deferred mRID is added, the reference gets resolved
service.add(junction)
print(junction in list(feeder.equipment)) # True

Why multiple services?​

You might be asking "Why not just one service for all identified objects?". Admittedly, just having one would be easier to work with. However, as models can become large, having every object in a single service (and thus process) will mean you need an ever increasing amount of RAM in your system. What we have done is separated out parts of the data model into separate concerns, at what we feel are sensible boundary points. That is:

  • The network service deals with models of the physical electricity network.
  • The customer service deals with customers and their agreements with electricity providers (e.g their Tariff).
  • The diagram service deals with things related to representing networks as diagrams (see Diagram and DiagramObject).
  • The measurement service deals with the values reported against measurements - MeasurementValues such as AnalogValue, AccumulatorValue and DiscreteValue - rather than the Measurement definitions themselves, which are held by the network service as part of the physical asset model. Keeping the values in their own service keeps frequently changing data out of the asset model.

Ultimately there is a tradeoff between developer usability and model size feasibility. Hopefully you will find this is a reasonable trade off when working with large systems.