# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
from typing import Union, List, Dict, Any, Tuple
import uuid
from VeraGridEngine.Devices.Parents.editable_device import EditableDevice
from VeraGridEngine.enumerations import DeviceType, VarPowerFlowReferenceType
from VeraGridEngine.Utils.Symbolic.symbolic import Var, Const, SharedVarReferenceType
def _new_uid() -> int:
"""Generate a fresh UUIDβv4 string."""
return uuid.uuid4().int
[docs]
class Connection:
"""
connection
"""
__slots__ = (
'non_mutable_uid',
'name',
'uid'
)
def __init__(self,
non_mutable_uid: int,
name: str,
uid: int):
self.non_mutable_uid = non_mutable_uid
self.name = name
self.uid = uid
[docs]
class VarFactory(EditableDevice):
"""
VarFactory
"""
__slots__ = (
'_var_dict',
'_const_dict',
'_diff_var_dict',
'_vars_info',
'_references_dict',
'_vars_references_dict',
'_vars_connected_dict',
)
def __init__(self,
idtag: Union[str, None] = "",
name: str = "Var Factory",
code: str = "",
comment: str = "") -> None:
"""
VarFactory
:param idtag: String. Element unique identifier
:param name: String. Contingency name
:param code: String. Contingency code name
:param comment: Comment
"""
EditableDevice.__init__(self,
idtag=idtag,
code=code,
name=name,
device_type=DeviceType.VarFactory,
comment=comment)
self._var_dict: Dict[int, Var] = dict()
self._const_dict: Dict[int, Const] = dict()
self._diff_var_dict: Dict[int, Var] = dict()
self._vars_info: Dict[Any, List[Var]] = dict()
self._references_dict: Dict[str, SharedVarReferenceType] = dict()
self._vars_references_dict: Dict[int, List[Var]] = dict()
self._vars_connected_dict = dict()
# self._vars_connected_dict: Dict[int, Dict[str, List[Var] | Dict[int, str] | Dict[int, int]]] = dict()
@property
def vars_info(self) -> Dict[Any, List[Var]]:
return self._vars_info
[docs]
def get_vars_to_save(self,
dev:Any,
names: List[str]) -> List[Var]:
"""
this function returns a list of variables which names are in names
:param dev:
:type dev:
:param names:
:type names:
:return:
:rtype:
"""
return [v for v in self._vars_info[dev] if v.name in names]
[docs]
def add_var(self,
name: str,
reference: VarPowerFlowReferenceType | None = None,
network_conn: bool = False,
shared_reference: str | None | SharedVarReferenceType = None,
non_mutable_uid: int | None = None,
uid: int | None = None,
diff_var: Var | None = None,
base_var: Var | None = None) -> Var:
"""
Adds a ver to the class
:param name:
:param shared_reference:
:param reference:
:param network_conn:
:param non_mutable_uid:
:param uid:
:param diff_var:
:param base_var:
:return:
"""
if isinstance(shared_reference, str):
if shared_reference not in self._references_dict:
self.create_reference(shared_reference)
self._vars_references_dict[self._references_dict[shared_reference].uid] = list()
v = Var(name=name, shared_reference=self._references_dict[shared_reference], reference=reference, network_conn=network_conn, non_mutable_uid=non_mutable_uid, uid=uid,
diff_var=None, base_var=None)
self.save_var_in_vars_references_dict(v, shared_reference)
self._var_dict[v.non_mutable_uid] = v
return v
else:
v = Var(name=name, reference=reference, network_conn=network_conn, shared_reference=shared_reference, uid=uid,
diff_var=None, base_var=None)
self._var_dict[v.non_mutable_uid] = v
return v
[docs]
def save_var_in_vars_references_dict(self, var:Var, reference:str):
self._vars_references_dict[self._references_dict[reference].uid].append(var)
[docs]
def create_reference(self, reference_name: str):
self._references_dict[reference_name] = SharedVarReferenceType(name=reference_name)
[docs]
def get_var(self, uid: int) -> Var | None:
"""
Gets a Var from the class
:param uid:
:return:
"""
if uid is not None:
return self._var_dict[uid]
else:
return None
[docs]
def add_diff_var(self,
name: str,
reference: VarPowerFlowReferenceType | None = None,
network_conn: bool = False,
shared_reference: str | None | SharedVarReferenceType = None,
non_mutable_uid: int | None = None,
uid: int | None = None,
diff_var: Var | None = None,
base_var: Var | None = None) -> Var:
"""
Adds a Diff ver to the class
:param name:
:param shared_reference:
:param non_mutable_uid:
:param network_conn:
:param uid:
:param diff_var:
:param base_var:
:return:
"""
if isinstance(shared_reference, str):
if shared_reference not in self._references_dict:
self.create_reference(shared_reference)
self._vars_references_dict[self._references_dict[shared_reference].uid] = list()
v = Var(name=name, shared_reference=self._references_dict[shared_reference], reference=reference, network_conn=network_conn, uid=uid, diff_var=diff_var, base_var=base_var)
self.save_var_in_vars_references_dict(v, shared_reference)
self._var_dict[v.non_mutable_uid] = v
self._diff_var_dict[v.non_mutable_uid] = v
return v
else:
v = Var(name=name, reference=reference, network_conn=network_conn, shared_reference=shared_reference, uid=uid, diff_var=diff_var, base_var=base_var)
self._diff_var_dict[v.non_mutable_uid] = v
return v
[docs]
def get_diff_var(self, uid: int) -> Var:
"""
Gets a Diff Var from the class
:param uid:
:return:
"""
return self._diff_var_dict[uid]
[docs]
def add_const(self,
value: float | None = None,
uid: int | None = None,
name: str = "")-> Const:
"""
Adds a Cont to the class
:param value:
:param uid:
:param name:
:return:
"""
v = Const(value=value, uid=uid, name=name)
self._const_dict[v.uid] = v
return v
[docs]
def connect_variables_by_uid(self, var_to_subs_non_mutable_uid: int, incoming_var_uid: int, incoming_var_name: str):
if var_to_subs_non_mutable_uid in self._var_dict:
self._var_dict[var_to_subs_non_mutable_uid].uid = incoming_var_uid
self._var_dict[var_to_subs_non_mutable_uid].name = incoming_var_name
elif var_to_subs_non_mutable_uid in self._diff_var_dict:
self._var_dict[var_to_subs_non_mutable_uid].uid = incoming_var_uid
self._var_dict[var_to_subs_non_mutable_uid].name = incoming_var_name
# recursitity for previous connected vars
if var_to_subs_non_mutable_uid in self._vars_connected_dict:
for connection in self._vars_connected_dict[var_to_subs_non_mutable_uid]:
self.connect_variables_by_uid(connection.non_mutable_uid, incoming_var_uid, incoming_var_name)
[docs]
def add_connection(self, var_to_subs: Var, incoming_var: Var):
if not incoming_var.non_mutable_uid in self._vars_connected_dict:
self._vars_connected_dict[incoming_var.non_mutable_uid] = list()
connection = Connection(var_to_subs.non_mutable_uid, var_to_subs.name, var_to_subs.uid)
self._vars_connected_dict[incoming_var.non_mutable_uid].append(connection)
self.connect_variables_by_uid(connection.non_mutable_uid, incoming_var.uid, incoming_var.name)
[docs]
def remove_connection(self, var_to_disconnect: Var, outgoing_var: Var):
for i, connection in enumerate(self._vars_connected_dict[outgoing_var.non_mutable_uid]):
if connection.non_mutable_uid == var_to_disconnect.non_mutable_uid:
self.connect_variables_by_uid(connection.non_mutable_uid, connection.uid, connection.name)
del self._vars_connected_dict[outgoing_var.non_mutable_uid][i]
[docs]
def add_connections(self, vars_to_subs: List[Var], incoming_vars: List[Var]):
pairs: List[Tuple[Var, Var]] = list(zip(vars_to_subs, incoming_vars))
var_to_subs: Var
incoming_var: Var
for var_to_subs, incoming_var in pairs:
self.add_connection(var_to_subs, incoming_var)
[docs]
def get_const(self, uid: int) -> Const:
"""
Gets a Cont from the class
:param uid:
:return:
"""
return self._const_dict[uid]
[docs]
def get_const_dict(self) -> Dict[int, Const]:
"""
:return:
"""
return self._const_dict
[docs]
def get_vars_dict(self) -> Dict[int, Var]:
"""
:return:
"""
return self._var_dict
[docs]
def get_diff_var_dict(self) -> Dict[int, Var]:
"""
:return:
"""
return self._diff_var_dict
[docs]
def get_references_dict(self) -> Dict[str, SharedVarReferenceType]:
"""
:return:
:rtype:
"""
return self._references_dict
[docs]
def get_connections_dict(self) -> Dict[int, List[Connection]]:
"""
:return:
:rtype:
"""
return self._vars_connected_dict
[docs]
def get_unique_template_name(self, name: str) -> str:
"""Return one template name that does not collide with existing symbols.
Some EMT builders reserve their symbolic namespace at the template level
before any variables are created. When the requested name is still free,
this helper returns it unchanged. Otherwise it appends an integer suffix.
:param name: Requested template name.
:return: Collision-free template name.
"""
existing_names = set(v.name for v in self._var_dict.values())
existing_names.update(v.name for v in self._diff_var_dict.values())
if name not in existing_names:
return name
else:
index: int = 1
candidate: str = f"{name}_{index}"
while candidate in existing_names:
index += 1
candidate = f"{name}_{index}"
return candidate
[docs]
def parse_const_dict(self, data_list: List[Dict[str, Any]]):
"""
:param data_list:
:return:
"""
obj_dict: Dict[int, Const] = dict()
for data in data_list:
t = data["type"]
if t == "Const":
if data.get("kind") == "complex":
arr = data["value"]
obj = Const(value=complex(arr[0], arr[1]),
uid=data["uid"])
else:
obj = Const(value=data["value"],
uid=data["uid"])
else:
raise ValueError(f"Unhandled symbolic primitive {t}")
# at this point we can store the object
obj_dict[obj.uid] = obj
self._const_dict = obj_dict
[docs]
def parse_var_dict(self, data_list: List[Dict[str, Any]]):
"""
:param data_list:
:return:
"""
obj_dict: Dict[int, Var] = dict()
for data in data_list:
assert data["type"] == "Var"
# Older persisted symbolic models may not store the power-flow
# reference field, so keep loading those files by defaulting to None.
key_ref = None
ref_data_dict: Any = data["shared_ref"]
if ref_data_dict is not None:
ref_data_name = ref_data_dict["name"]
ref_data_uid = ref_data_dict["uid"]
key_ref: SharedVarReferenceType | None
if ref_data_name is not None and ref_data_uid is not None:
if ref_data_name not in self._references_dict:
key_ref = SharedVarReferenceType(ref_data_name, ref_data_uid)
if key_ref is not None:
self._references_dict[ref_data_name] = key_ref
else:
key_ref = self._references_dict[ref_data_name]
else:
key_ref = None
ref_power_flow_data: Any = data.get("ref", None)
key_power_flow_ref: VarPowerFlowReferenceType | None
if ref_power_flow_data is not None:
key_power_flow_ref = VarPowerFlowReferenceType(ref_power_flow_data)
else:
key_power_flow_ref = None
obj = Var(name=data["name"], uid=data["uid"], shared_reference=key_ref, reference=key_power_flow_ref, non_mutable_uid=data["non_mutable_uid"])
if ref_data_dict is not None:
ref_data_uid = ref_data_dict["uid"]
if ref_data_uid is not None:
if ref_data_uid in self._vars_references_dict:
self._vars_references_dict[ref_data_uid].append(obj)
else:
self._vars_references_dict[ref_data_uid] = list()
self._vars_references_dict[ref_data_uid].append(obj)
# at this point we can store the object
obj_dict[obj.non_mutable_uid] = obj
self._var_dict = obj_dict
[docs]
def parse_diff_var_dict(self,
data_list: List[Dict[str, Any]]) -> None:
"""
Parse persisted differential symbolic variables into the factory.
The loader must rebuild each differential variable using the exact
symbolic metadata stored in the ``.veragrid`` payload. In particular,
the power-flow reference has to be converted back to the
``VarPowerFlowReferenceType`` enum so downstream EMT connection logic
can distinguish branch terminal references such as ``vf_A`` and
``vt_A`` from bus references such as ``v_A``.
:param data_list: Serialized differential variable records.
:return: None.
"""
obj_dict: Dict[int, Var | Const | Var] = dict()
for data in data_list:
assert data["type"] == "DiffVar"
"""
lst.append({
"type": "DiffVar",
"name": expr.name,
"uid": expr.uid,
"base_var": _expr_to_dict(expr=expr.base_var, obj_dict=obj_dict),
})
"""
# Recover the base variable first because differential variables
# are linked to the already reconstructed algebraic/differential
# chain. This preserves the original derivative hierarchy.
if data["base_var"] in self._var_dict.keys():
base_var = self._var_dict[data["base_var"]]
elif data["base_var"] in self._diff_var_dict.keys():
base_var = self._diff_var_dict[data["base_var"]]
else:
base_var = obj_dict[data["base_var"]]
# Older persisted symbolic models may not store the power-flow
# reference field on differential variables either.
key_ref: SharedVarReferenceType | None = None
ref_data_dict: Any = data["shared_ref"]
if ref_data_dict is not None:
ref_data_name = ref_data_dict["name"]
ref_data_uid = ref_data_dict["uid"]
if ref_data_name is not None and ref_data_uid is not None:
if ref_data_name not in self._references_dict:
key_ref = SharedVarReferenceType(ref_data_name, ref_data_uid)
if key_ref is not None:
self._references_dict[ref_data_name] = key_ref
else:
pass
else:
key_ref = self._references_dict[ref_data_name]
else:
key_ref = None
else:
pass
# Convert the persisted textual reference back to the enum object.
# Keeping the enum identity is required so EMT topology validation
# can still recognize branch-side references after reopening files.
reference_power_flow_data: Any = data.get("ref", None)
reference_power_flow: VarPowerFlowReferenceType | None
if reference_power_flow_data is not None:
reference_power_flow = VarPowerFlowReferenceType(reference_power_flow_data)
else:
reference_power_flow = None
obj = Var(name=data["name"],
uid=data["uid"],
base_var=base_var,
shared_reference=key_ref,
reference=reference_power_flow)
if ref_data_dict is not None:
ref_data_uid = ref_data_dict["uid"]
if ref_data_uid is not None:
if ref_data_uid in self._vars_references_dict:
self._vars_references_dict[ref_data_uid].append(obj)
else:
self._vars_references_dict[ref_data_uid] = list()
self._vars_references_dict[ref_data_uid].append(obj)
else:
pass
else:
pass
# at this point we can store the object
obj_dict[obj.non_mutable_uid] = obj
self._diff_var_dict = obj_dict
[docs]
def parse_references_dict(self, datalist: List[Dict[str, Any]]):
"""
Parse references list into _references_dict and _vars_references_dict.
:param datalist: List of dicts with "type", "name", "uid" for share_reference.
:return:
"""
for data in datalist:
if data["type"] == "share_reference":
ref = SharedVarReferenceType(name=data["name"], uid=data["uid"])
self._references_dict[data["name"]] = ref
self._vars_references_dict[ref.uid] = list()
[docs]
def parse_connections_dict(self, datalist: Dict[int, List[Any]]) -> None:
"""
Parse serialized connections and replay their runtime alias state.
The saved connection table stores the graph that links one incoming
variable to one or more substituted variables through their stable
non-mutable identities. Rebuilding only the graph is not sufficient for
the runtime symbolic system, because fresh GUI-created models also apply
UID/name aliasing immediately when the connection is created. The load
path must therefore reconstruct the graph first and then replay the same
alias propagation so reopened symbolic models behave like newly created
ones.
:param datalist: List of dicts with block_uid -> list of connection dicts.
:return: None.
"""
uid_key: int
connections_list: List[Any]
conn_data: Any
for uid, connections_list in datalist.items():
uid_key = int(uid)
if uid_key not in self._vars_connected_dict:
self._vars_connected_dict[uid_key] = list()
else:
pass
for conn_data in connections_list:
if conn_data["type"] == "Connection":
conn: Connection = Connection(
non_mutable_uid=conn_data["non_mutable_uid"],
name=conn_data["name"],
uid=conn_data["uid"]
)
self._vars_connected_dict[uid_key].append(conn)
else:
pass
# Replay the alias propagation only after the full graph has been
# reconstructed. This mirrors the live connection workflow while also
# ensuring recursive downstream links can be traversed during replay.
incoming_non_mutable_uid: int
connection_list: List[Connection]
connection: Connection
for incoming_non_mutable_uid, connection_list in self._vars_connected_dict.items():
if incoming_non_mutable_uid in self._var_dict:
incoming_var: Var = self._var_dict[incoming_non_mutable_uid]
elif incoming_non_mutable_uid in self._diff_var_dict:
incoming_var = self._diff_var_dict[incoming_non_mutable_uid]
else:
incoming_var = None
if incoming_var is not None:
for connection in connection_list:
self.connect_variables_by_uid(
connection.non_mutable_uid,
incoming_var.uid,
incoming_var.name,
)
else:
pass
[docs]
def register_var(self, dev:Any, var:Var):
"""
Associate a variable with a device
:param dev: Device
:param var: Variable
"""
var_list = self._vars_info.get(dev, None)
if var_list is None:
self._vars_info[dev] = [var]
else:
var_list.append(var)
[docs]
def find_var_or_diff_var(self, var_uid: int | None):
if var_uid in self.get_vars_dict():
return self.get_var(var_uid)
elif var_uid in self.get_diff_var_dict():
return self.get_diff_var(var_uid)
elif var_uid is None:
return None
else:
raise ValueError(f"Var with uid:{var_uid} not added in VarFactory as var or diff_var.")