Skip to content

Commit c46c331

Browse files
authored
Release of version 2025.06.0 (#187)
Release of version 2025.06.0
2 parents 3dc09d1 + 3a13360 commit c46c331

13 files changed

Lines changed: 332 additions & 81 deletions

File tree

gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
from gridappsd_field_bus.field_interface.agents.agents import (FeederAgent, DistributedAgent,
44
CoordinatingAgent, SwitchAreaAgent,
5-
SecondaryAreaAgent, SubstationAgent)
5+
SecondaryAreaAgent, SubstationAgent, compute_req)
66

77
__all__: List[str] = ["FeederAgent", "DistributedAgent", "CoordinatingAgent"]

gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/agents/agents.py

Lines changed: 159 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
from datetime import datetime
77
from typing import Dict
88

9+
import time
10+
import os
11+
from functools import wraps
12+
import sys
13+
import inspect
14+
import atexit
15+
916
from cimgraph.databases import ConnectionParameters
1017
from cimgraph.databases.gridappsd import GridappsdConnection
1118
from cimgraph.models import FeederModel
@@ -17,12 +24,18 @@
1724
from gridappsd_field_bus.field_interface.gridappsd_field_bus import GridAPPSDMessageBus
1825
from gridappsd_field_bus.field_interface.interfaces import (FieldMessageBus, MessageBusDefinition, MessageBusFactory)
1926

27+
2028
CIM_PROFILE = None
2129
IEC61970_301 = None
2230
cim = None
2331

2432
_log = logging.getLogger(__name__)
25-
33+
decorator_logger = logging.getLogger("decorator_logger")
34+
decorator_logger.setLevel(logging.INFO)
35+
file_handler = logging.FileHandler("compute_req_log.txt") # Log file name
36+
formatter = logging.Formatter('[COMPUTE_REQ] %(asctime)s - %(message)s')
37+
file_handler.setFormatter(formatter)
38+
decorator_logger.addHandler(file_handler)
2639

2740
def set_cim_profile(cim_profile: str, iec61970_301: int):
2841
global CIM_PROFILE
@@ -41,6 +54,147 @@ class AgentRegistrationDetails:
4154
upstream_message_bus_id: FieldMessageBus.id
4255
downstream_message_bus_id: FieldMessageBus.id
4356

57+
@atexit.register
58+
def call_counter_report():
59+
decorator_logger.info("Function call counts summary:")
60+
for func_name, count in function_call_counts.items():
61+
decorator_logger.info(f"{func_name} was called {count} time(s)")
62+
63+
@atexit.register
64+
def message_size_report():
65+
decorator_logger.info("Total message size summary:")
66+
for func_name, total_size in message_size_totals.items():
67+
decorator_logger.info(f"{func_name} total message size: {total_size} bytes")
68+
69+
def compute_req(cls):
70+
functions = [
71+
'__init__',
72+
#'on_measurement',
73+
'on_upstream_message',
74+
'on_downstream_message',
75+
'on_request',
76+
'publish_upstream',
77+
'publish_downstream',
78+
'send_control_command'
79+
]
80+
81+
def call_counter(func):
82+
name = func.__qualname__
83+
84+
@wraps(func)
85+
def wrapper(*args, **kwargs):
86+
if args[0].agent_id+'.'+name not in function_call_counts:
87+
function_call_counts[args[0].agent_id+'.'+name] = 0
88+
function_call_counts[args[0].agent_id+'.'+name] += 1
89+
#decorator_logger.info(f"{name} called {function_call_counts[name]} times")
90+
return func(*args, **kwargs)
91+
return wrapper
92+
93+
def timed(func):
94+
@wraps(func)
95+
def wrapper(*args, **kwargs):
96+
start = time.perf_counter()
97+
result = func(*args, **kwargs)
98+
end = time.perf_counter()
99+
class_name = args[0].__class__.__name__ if args else ""
100+
if func.__name__ == '__init__':
101+
decorator_logger.info(f"{class_name}.{func.__name__}.{args[0].agent_id} took: {end - start:.6f} seconds")
102+
return result
103+
return wrapper
104+
105+
def get_deep_size(func):
106+
@wraps(func)
107+
def wrapper(*args, **kwargs):
108+
result = func(*args, **kwargs)
109+
110+
def deep_size(obj, seen=None):
111+
if seen is None:
112+
seen = set()
113+
obj_id = id(obj)
114+
if obj_id in seen:
115+
return 0
116+
seen.add(obj_id)
117+
size = sys.getsizeof(obj)
118+
if isinstance(obj, dict):
119+
size += sum(deep_size(k, seen) + deep_size(v, seen) for k, v in obj.items())
120+
elif isinstance(obj, (list, tuple, set, frozenset)):
121+
size += sum(deep_size(i, seen) for i in obj)
122+
elif hasattr(obj, '__dict__'):
123+
for attr_name, attr_value in vars(obj).items():
124+
if attr_name in ['feeder_area', 'switch_area', 'secondary_area']:
125+
continue
126+
size += deep_size(attr_value, seen)
127+
elif hasattr(obj, '__slots__'):
128+
size += sum(deep_size(getattr(obj, slot), seen) for slot in obj.__slots__ if hasattr(obj, slot))
129+
return size
130+
131+
self = args[0]
132+
obj_size = deep_size(self)
133+
decorator_logger.info(f"{self.__class__.__name__}.{func.__name__}.{args[0].agent_id} size is: {obj_size} bytes")
134+
135+
return result
136+
return wrapper
137+
138+
def get_graph_size(func):
139+
@wraps(func)
140+
def wrapper(*args, **kwargs):
141+
self = args[0]
142+
result = func(*args, **kwargs)
143+
area_names = ['feeder_area', 'switch_area', 'secondary_area']
144+
area_found = False
145+
for name in area_names:
146+
area_dict = getattr(self, name, None)
147+
if area_dict is not None and hasattr(area_dict, 'graph'):
148+
graph_keys = [key.__name__ for key in list(area_dict.graph.keys())]
149+
size = len(area_dict.graph.keys())
150+
decorator_logger.info(f"{self.__class__.__name__}.{func.__name__}.{args[0].agent_id} length of graph: {size}")
151+
decorator_logger.info(f"{self.__class__.__name__}.{name}.{args[0].agent_id} graph keys: {graph_keys}")
152+
area_found = True
153+
break
154+
155+
if not area_found:
156+
decorator_logger.error(f"{class_name}.{func.__name__}.{args[0].agent_id} No area dictionary (feeder/switch/secondary) found in {self.__class__.__name__}")
157+
return result
158+
return wrapper
159+
160+
def log_message_size(func):
161+
name = func.__qualname__
162+
163+
@wraps(func)
164+
def wrapper(*args, **kwargs):
165+
sig = inspect.signature(func)
166+
bound_args = sig.bind(*args, **kwargs)
167+
bound_args.apply_defaults()
168+
169+
if 'message' in bound_args.arguments:
170+
msg = bound_args.arguments['message']
171+
size = sys.getsizeof(msg)
172+
if args[0].agent_id+'.'+name not in message_size_totals:
173+
message_size_totals[args[0].agent_id+'.'+name] = 0
174+
message_size_totals[args[0].agent_id+'.'+name] += size
175+
176+
if 'differenceBuilder' in bound_args.arguments:
177+
msg = bound_args.arguments['differenceBuilder']
178+
size = sys.getsizeof(msg)
179+
if args[0].agent_id+'.'+name not in message_size_totals:
180+
message_size_totals[args[0].agent_id+'.'+name] = 0
181+
message_size_totals[args[0].agent_id+'.'+name] += size
182+
183+
return func(*args, **kwargs)
184+
return wrapper
185+
186+
# Decorate the relevant functions
187+
for attr_name in functions:
188+
if hasattr(cls, attr_name):
189+
original_func = getattr(cls, attr_name)
190+
if callable(original_func):
191+
if attr_name == '__init__':
192+
decorated = get_deep_size(get_graph_size(timed(original_func)))
193+
else:
194+
decorated = call_counter(log_message_size(timed(original_func)))
195+
setattr(cls, attr_name, decorated)
196+
197+
return cls
44198

45199
class DistributedAgent:
46200

@@ -61,10 +215,7 @@ def __init__(self,
61215
self.simulation_id = simulation_id
62216
self.context = None
63217

64-
# TODO: Change params and connection to local connection
65-
self.params = ConnectionParameters(cim_profile=CIM_PROFILE, iec61970_301=IEC61970_301)
66-
67-
self.connection = GridappsdConnection(self.params)
218+
self.connection = GridappsdConnection()
68219
self.connection.cim_profile = cim_profile
69220

70221
self.app_id = agent_config['app_id']
@@ -79,14 +230,10 @@ def __init__(self,
79230
self.agent_area_dict = agent_area_dict
80231

81232
if upstream_message_bus_def is not None:
82-
if upstream_message_bus_def.is_ot_bus:
83-
self.upstream_message_bus = MessageBusFactory.create(upstream_message_bus_def)
84-
# else:
85-
# self.upstream_message_bus = VolttronMessageBus(upstream_message_bus_def)
86-
233+
self.upstream_message_bus = MessageBusFactory.create(upstream_message_bus_def)
234+
87235
if downstream_message_bus_def is not None:
88-
if downstream_message_bus_def.is_ot_bus:
89-
self.downstream_message_bus = MessageBusFactory.create(downstream_message_bus_def)
236+
self.downstream_message_bus = MessageBusFactory.create(downstream_message_bus_def)
90237

91238
if self.downstream_message_bus is None and self.upstream_message_bus is None:
92239
raise ValueError("Must have at least a downstream and/or upstream message bus specified")

gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/field_proxy_forwarder.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
from gridappsd import GridAPPSD
66
from gridappsd import topics
77

8+
from cimgraph.databases import GridappsdConnection, BlazegraphConnection
9+
from cimgraph.models import BusBranchModel, FeederModel
10+
11+
import os
12+
import cimgraph.utils as utils
13+
import cimgraph.data_profile.cimhub_ufls as cim
14+
815
REQUEST_FIELD = ".".join((topics.PROCESS_PREFIX, "request.field"))
916

1017
class FieldListener:
@@ -16,7 +23,7 @@ def __init__(self, ot_connection: GridAPPSD, proxy_connection: stomp.Connection)
1623
def on_message(self, headers, message):
1724
"Receives messages coming from Proxy bus (e.g. ARTEMIS) and forwards to OT bus"
1825
try:
19-
print(f"Received message at Proxy: {message}")
26+
print(f"Received message at Proxy. destination: {headers['destination']}, message: {headers}")
2027

2128
if headers["destination"] == topics.field_output_topic():
2229
self.ot_connection.send(topics.field_output_topic(), message)
@@ -29,8 +36,11 @@ def on_message(self, headers, message):
2936
request_type = request_data.get("request_type")
3037
if request_type == "get_context":
3138
response = self.ot_connection.get_response(headers["destination"],message)
32-
self.proxy_connection.send(headers["reply_to"],response)
33-
39+
self.proxy_connection.send(headers["reply-to"],response)
40+
elif request_type == "start_publishing":
41+
response = self.ot_connection.get_response(headers["destination"],message)
42+
self.proxy_connection.send(headers["reply-to"],json.dumps(response))
43+
3444
else:
3545
print(f"Unrecognized message received by Proxy: {message}")
3646

@@ -43,7 +53,7 @@ class FieldProxyForwarder:
4353
when direct connection is not possible.
4454
"""
4555

46-
def __init__(self, connection_url: str, username: str, password: str):
56+
def __init__(self, connection_url: str, username: str, password: str, mrid :str):
4757

4858
#Connect to OT
4959
self.ot_connection = GridAPPSD()
@@ -52,27 +62,60 @@ def __init__(self, connection_url: str, username: str, password: str):
5262
self.broker_url = connection_url
5363
self.username = username
5464
self.password = password
55-
self.proxy_connection = stomp.Connection([(self.broker_url.split(":")[0], int(self.broker_url.split(":")[1]))],keepalive=True)
65+
self.proxy_connection = stomp.Connection([(self.broker_url.split(":")[0], int(self.broker_url.split(":")[1]))],keepalive=True, heartbeats=(10000,10000))
5666
self.proxy_connection.set_listener('', FieldListener(self.ot_connection, self.proxy_connection))
5767
self.proxy_connection.connect(self.username, self.password, wait=True)
68+
5869
print('Connected to Proxy')
5970

6071

6172

6273
#Subscribe to messages from field
6374
self.proxy_connection.subscribe(destination=topics.BASE_FIELD_TOPIC+'.*', id=1, ack="auto")
64-
75+
self.proxy_connection.subscribe(destination='goss.gridappsd.process.request.*', id=2, ack="auto")
76+
6577
#Subscribe to messages on OT bus
6678
self.ot_connection.subscribe(topics.field_input_topic(), self.on_message_from_ot)
6779

80+
81+
82+
os.environ['CIMG_CIM_PROFILE'] = 'cimhub_ufls'
83+
os.environ['CIMG_URL'] = 'http://localhost:8889/bigdata/namespace/kb/sparql'
84+
os.environ['CIMG_DATABASE'] = 'powergridmodel'
85+
os.environ['CIMG_NAMESPACE'] = 'http://iec.ch/TC57/CIM100#'
86+
os.environ['CIMG_IEC61970_301'] = '8'
87+
os.environ['CIMG_USE_UNITS'] = 'False'
88+
89+
self.database = BlazegraphConnection()
90+
distribution_area = cim.DistributionArea(mRID=mrid)
91+
self.network = BusBranchModel(
92+
connection=self.database,
93+
container=distribution_area,
94+
distributed=False)
95+
self.network.get_all_edges(cim.DistributionArea)
96+
self.network.get_all_edges(cim.Substation)
97+
98+
for substation in self.network.graph.get(cim.Substation,{}).values():
99+
print(f'Subscribing to Substation: /topic/goss.gridappsd.field.{substation.mRID}')
100+
self.ot_connection.subscribe('/topic/goss.gridappsd.field.'+substation.mRID, self.on_message_from_ot)
101+
102+
103+
104+
#self.ot_connection.subscribe(topics.BASE_FIELD_TOPIC, self.on_message_from_ot)
105+
106+
68107
def on_message_from_ot(self, headers, message):
108+
69109
"Receives messages coming from OT bus (GridAPPS-D) and forwards to Proxy bus"
70110
try:
71111
print(f"Received message from OT: {message}")
72112

73113
if headers["destination"] == topics.field_input_topic():
74-
self.proxy_connection.send(topics.field_input_topic(), message)
114+
self.proxy_connection.send(topics.field_input_topic(),json.dumps(message))
115+
116+
elif 'goss.gridappsd.field' in headers["destination"]:
75117

118+
self.proxy_connection.send(headers["destination"],json.dumps(message))
76119
else:
77120
print(f"Unrecognized message received by OT: {message}")
78121

@@ -86,12 +129,14 @@ def on_message_from_ot(self, headers, message):
86129
parser.add_argument("username")
87130
parser.add_argument("passwd")
88131
parser.add_argument("connection_url")
132+
parser.add_argument("mrid")
89133
opts = parser.parse_args()
90134
proxy_connection_url = opts.connection_url
91135
proxy_username = opts.username
92136
proxy_password = opts.passwd
137+
mrid = opts.mrid
93138

94-
proxy_forwarder = FieldProxyForwarder(proxy_connection_url, proxy_username, proxy_password)
139+
proxy_forwarder = FieldProxyForwarder(proxy_connection_url, proxy_username, proxy_password, mrid)
95140

96141
while True:
97142
time.sleep(0.1)

gridappsd-field-bus-lib/gridappsd_field_bus/field_interface/gridappsd_field_bus.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(self, definition: MessageBusDefinition):
1313
self._user = definition.connection_args["GRIDAPPSD_USER"]
1414
self._password = definition.connection_args["GRIDAPPSD_PASSWORD"]
1515
self._address = definition.connection_args["GRIDAPPSD_ADDRESS"]
16+
self._use_auth_token = definition.connection_args.get("GRIDAPPSD_USE_TOKEN_AUTH", False)
1617

1718
self.gridappsd_obj = None
1819

@@ -29,7 +30,7 @@ def connect(self):
2930
"""
3031
Connect to the concrete message bus that implements this interface.
3132
"""
32-
self.gridappsd_obj = GridAPPSD()
33+
self.gridappsd_obj = GridAPPSD(use_auth_token=self._use_auth_token)
3334

3435
def subscribe(self, topic, callback):
3536
if self.gridappsd_obj is not None:

0 commit comments

Comments
 (0)