Source code for VeraGridEngine.Devices.Diagrams.base_diagram

# 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
import copy
import sys
import uuid
import networkx as nx
from typing import Any, Dict, Union, List, Tuple, TYPE_CHECKING

from VeraGridEngine.Devices.Diagrams.graphic_location import GraphicLocation
from VeraGridEngine.Devices.Diagrams.map_location import MapLocation
from VeraGridEngine.enumerations import DiagramType, DeviceType, BusGraphicType
from VeraGridEngine.basic_structures import Logger
from VeraGridEngine.enumerations import Colormaps

if TYPE_CHECKING:
    from VeraGridEngine.Devices.types import ALL_DEV_TYPES
    from VeraGridEngine.Devices.Substation.bus import Bus
    from VeraGridEngine.Devices.Substation.busbar import BusBar
    from VeraGridEngine.Devices.Fluid.fluid_node import FluidNode
    from VeraGridEngine.Devices.Fluid.fluid_path import FluidPath
    from VeraGridEngine.Devices.Branches.transformer3w import Transformer3W
    from VeraGridEngine.Devices.Branches.transformerNw import TransformerNW


[docs] def get_layout_anchor(api_object: ALL_DEV_TYPES) -> ALL_DEV_TYPES: """ Return the electrical anchor used to infer schematic layout metadata. :param api_object: Diagram node API object. :return: Anchor object carrying the electrical metadata for layout purposes. """ anchor: ALL_DEV_TYPES = api_object # Fluid nodes are placed using the electrical bus they are attached to. if api_object.device_type == DeviceType.FluidNodeDevice: if api_object.bus is not None: anchor = api_object.bus else: anchor = api_object # Multi-winding transformers are placed using their internal anchor bus. elif api_object.device_type == DeviceType.Transformer3WDevice: if api_object.bus0 is not None: anchor = api_object.bus0 else: anchor = api_object elif api_object.device_type == DeviceType.TransformerNwDevice: if api_object.bus0 is not None: anchor = api_object.bus0 else: anchor = api_object # All other devices already expose the relevant electrical metadata directly. else: anchor = api_object return anchor
[docs] def get_layout_voltage_kv(api_object: ALL_DEV_TYPES) -> float: """ Get the voltage used to assign schematic layers. :param api_object: Diagram node API object. :return: Voltage in kV used by the automatic layout. """ anchor: ALL_DEV_TYPES = get_layout_anchor(api_object=api_object) voltage_kv: float = 0.0 # Buses already expose the nominal voltage directly. if anchor.device_type == DeviceType.BusDevice: voltage_kv = float(anchor.Vnom) # Bus bars obtain the voltage from their voltage level when that data exists. elif anchor.device_type == DeviceType.BusBarDevice: if anchor.voltage_level is not None: voltage_kv = float(anchor.voltage_level.Vnom) else: voltage_kv = 0.0 # All remaining node types fall back to zero when there is no electrical level. else: voltage_kv = 0.0 return voltage_kv
[docs] def get_layout_is_slack(api_object: ALL_DEV_TYPES) -> bool: """ Determine whether one diagram node should be treated as the island root candidate. :param api_object: Diagram node API object. :return: ``True`` when the node is electrically slack. """ anchor: ALL_DEV_TYPES = get_layout_anchor(api_object=api_object) is_slack: bool = False if anchor.device_type == DeviceType.BusDevice: is_slack = bool(anchor.is_slack) else: is_slack = False return is_slack
[docs] def get_layout_is_dc(api_object: ALL_DEV_TYPES) -> bool: """ Determine whether one diagram node belongs to the DC side of the network. :param api_object: Diagram node API object. :return: ``True`` when the node is DC. """ anchor: ALL_DEV_TYPES = get_layout_anchor(api_object=api_object) is_dc: bool = False if anchor.device_type == DeviceType.BusDevice: is_dc = bool(anchor.is_dc) else: is_dc = False return is_dc
[docs] def get_layout_substation_name(api_object: ALL_DEV_TYPES) -> str: """ Get the substation name used to stabilize the ordering inside one layer. :param api_object: Diagram node API object. :return: Substation name or an empty string. """ anchor: ALL_DEV_TYPES = get_layout_anchor(api_object=api_object) substation_name: str = "" if anchor.device_type == DeviceType.BusDevice: if anchor.substation is not None: substation_name = anchor.substation.name else: substation_name = "" elif anchor.device_type == DeviceType.BusBarDevice: if anchor.voltage_level is not None and anchor.voltage_level.substation is not None: substation_name = anchor.voltage_level.substation.name else: substation_name = "" else: substation_name = "" return substation_name
[docs] def add_node_layout_data(graph: nx.MultiDiGraph, node_index: int, api_object: ALL_DEV_TYPES) -> None: """ Add one node together with the electrical metadata needed by the schematic layouts. :param graph: Diagram graph under construction. :param node_index: Integer node identifier. :param api_object: Diagram node API object. """ device_tpe: str = api_object.device_type.value voltage_kv: float = get_layout_voltage_kv(api_object=api_object) is_slack: bool = get_layout_is_slack(api_object=api_object) is_dc: bool = get_layout_is_dc(api_object=api_object) substation_name: str = get_layout_substation_name(api_object=api_object) node_name: str = api_object.name # The graph keeps the original API object together with the explicit metadata # required by the layout stage so the GUI does not need to infer it again. graph.add_node(node_index, api_object=api_object, device_tpe=device_tpe, voltage_kv=voltage_kv, is_slack=is_slack, is_dc=is_dc, substation=substation_name, name=node_name)
[docs] def get_branch_endpoint_voltages(branch: ALL_DEV_TYPES) -> Tuple[float, float]: """ Get the endpoint voltages used to detect inter-level links. :param branch: Branch API object. :return: Pair of endpoint voltages in kV. """ source_voltage_kv: float = 0.0 target_voltage_kv: float = 0.0 # Fluid links use fluid nodes as endpoints, which in turn derive the voltage from their bus. if branch.device_type == DeviceType.FluidPathDevice: source_voltage_kv = get_layout_voltage_kv(api_object=branch.source) target_voltage_kv = get_layout_voltage_kv(api_object=branch.target) # Electrical branches expose bus_from and bus_to directly. else: source_voltage_kv = get_layout_voltage_kv(api_object=branch.bus_from) target_voltage_kv = get_layout_voltage_kv(api_object=branch.bus_to) return source_voltage_kv, target_voltage_kv
[docs] def add_branch_layout_edge(graph: nx.MultiDiGraph, from_idx: int, to_idx: int, branch: ALL_DEV_TYPES, dev_tpe: DeviceType, raw_impedance: float) -> None: """ Add one branch edge with both physical distance and spring attraction metadata. :param graph: Diagram graph under construction. :param from_idx: Source node index. :param to_idx: Target node index. :param branch: Branch API object. :param dev_tpe: Branch device type. :param raw_impedance: Physical distance surrogate for impedance-aware layouts. """ impedance: float = max(abs(float(raw_impedance)), 1e-6) strength: float = min(1.0 / impedance, 50.0) source_voltage_kv: float target_voltage_kv: float source_voltage_kv, target_voltage_kv = get_branch_endpoint_voltages(branch=branch) # The edge stores two different magnitudes because impedance-based layouts # and spring-based layouts interpret edge lengths in opposite directions. graph.add_edge(from_idx, to_idx, key=branch.idtag, branch_id=branch.idtag, kind=dev_tpe.value, impedance=impedance, strength=strength, weight=strength, voltage_delta_kv=abs(source_voltage_kv - target_voltage_kv))
[docs] class PointsGroup: """ Diagram """ def __init__(self, name: str = '') -> None: """ :param name: Diagram name """ self.name = name # device_type: {device uuid: {x, y, h, w, r}} self.locations: Dict[str, Union[GraphicLocation, MapLocation]] = dict()
[docs] def set_point(self, device: ALL_DEV_TYPES, location: Union[GraphicLocation, MapLocation]): """ :param device: :param location: :return: """ self.locations[device.idtag] = location
[docs] def delete_device(self, device: ALL_DEV_TYPES): """ Delete location :param device: :return: """ loc = self.query_point(device) if loc: del self.locations[device.idtag] else: return None
[docs] def query_point(self, device: ALL_DEV_TYPES) -> Union[GraphicLocation, MapLocation, None]: """ :param device: :return: """ return self.locations.get(device.idtag, None)
[docs] def get_dict(self) -> Dict[str, Union[GraphicLocation, MapLocation]]: """ :return: """ points = {idtag: location.get_properties_dict() for idtag, location in self.locations.items()} return points
[docs] def copy(self, obj_dict: Dict[str, ALL_DEV_TYPES] | None = None) -> "PointsGroup": """ Copy the locations while treating API objects as pointers. If an object dictionary is supplied, locations are rebound to the matching object in that dictionary by idtag. Otherwise, the current API object pointer is preserved. """ cpy = PointsGroup(name=self.name) for idtag, location in self.locations.items(): api_object = location.api_object if obj_dict is not None: api_object = obj_dict.get(idtag, api_object) cpy.locations[idtag] = location.copy(api_object=api_object) return cpy
def __deepcopy__(self, memo: Dict[int, Any]) -> "PointsGroup": """ Deep-copy the group layout without deep-copying pointed API objects. """ if id(self) in memo: return memo[id(self)] cpy = self.copy() memo[id(self)] = cpy return cpy
[docs] def parse_data(self, data: Dict[str, Dict[str, Any]], obj_dict: Dict[str, ALL_DEV_TYPES], logger: Logger, category: str = "") -> None: """ Parse file data ito this class :param data: json dictionary :param obj_dict: dictionary of relevant objects (idtag, object) :param logger: Logger :param category: category """ self.locations = dict() for idtag, location in data.items(): api_object = obj_dict.get(idtag, None) if api_object is None: # locations with no API object are not created logger.add_error("Diagram location could not find API object", device_class=category, device=idtag, ) else: if 'x' in location: self.locations[idtag] = GraphicLocation(x=location['x'], y=location['y'], w=location['w'], h=location['h'], r=location['r'], poly_line=location.get('poly_line', list()), layout_metadata=location.get('layout_metadata', dict()), draw_labels=location.get('draw_labels', True), api_object=api_object) if 'latitude' in location: self.locations[idtag] = MapLocation(latitude=location['latitude'], longitude=location['longitude'], altitude=location['altitude'], draw_labels=location.get('draw_labels', True), api_object=api_object)
[docs] class BaseDiagram: """ Diagram """ def __init__(self, idtag: Union[str, None], name: str, diagram_type: DiagramType = DiagramType, use_flow_based_width: bool = False, min_branch_width: int = 1.0, max_branch_width=5, min_bus_width=1.0, max_bus_width=20, arrow_size=20, palette: Colormaps = Colormaps.VeraGrid, default_bus_voltage: float = 10): """ :param idtag: :param name: :param diagram_type: :param use_flow_based_width: :param min_branch_width: :param max_branch_width: :param min_bus_width: :param max_bus_width: :param arrow_size: :param palette: :param default_bus_voltage: """ if idtag is None: self.idtag = uuid.uuid4().hex else: self.idtag = idtag.replace('_', '').replace('-', '') self.name = name # device_type: {device uuid: {x, y, h, w, r}} self.data: Dict[str, PointsGroup] = dict() # diagram type: Map or Schematic, ... self.diagram_type: DiagramType = diagram_type # sizes self._use_flow_based_width: bool = use_flow_based_width self._min_branch_width: float = min_branch_width self._max_branch_width: float = max_branch_width self._min_bus_width: float = min_bus_width self._max_bus_width: float = max_bus_width self._arrow_size: float = arrow_size self._use_api_colors: bool = False self._palette = palette self._default_bus_voltage: float = default_bus_voltage
[docs] def copy(self, obj_dict: Dict[str, Dict[str, ALL_DEV_TYPES]] | None = None) -> "BaseDiagram": """ Copy the diagram layout while treating API objects as pointers. :param obj_dict: Optional dictionary by device type and idtag used to rebind locations to objects in a target circuit. :return: A copied diagram with detached layout containers. """ cpy = self.__class__.__new__(self.__class__) for attr_name, attr_value in self.__dict__.items(): if attr_name == "data": continue setattr(cpy, attr_name, copy.deepcopy(attr_value)) cpy.data = dict() for category, points_group in self.data.items(): category_obj_dict = None if obj_dict is None else obj_dict.get(category, None) cpy.data[category] = points_group.copy(obj_dict=category_obj_dict) return cpy
def __deepcopy__(self, memo: Dict[int, Any]) -> "BaseDiagram": """ Make generic deepcopy safe for diagrams by preserving API object pointers. """ if id(self) in memo: return memo[id(self)] cpy = self.copy() memo[id(self)] = cpy return cpy @property def use_flow_based_width(self) -> bool: """ :return: """ return self._use_flow_based_width @use_flow_based_width.setter def use_flow_based_width(self, value: bool): self._use_flow_based_width = value # min_branch_width property @property def min_branch_width(self) -> float: """ :return: """ return self._min_branch_width @min_branch_width.setter def min_branch_width(self, value: float): self._min_branch_width = value # max_branch_width property @property def max_branch_width(self) -> float: """ :return: """ return self._max_branch_width @max_branch_width.setter def max_branch_width(self, value: float): self._max_branch_width = value # min_bus_width property @property def min_bus_width(self) -> float: """ :return: """ return self._min_bus_width @min_bus_width.setter def min_bus_width(self, value: float): self._min_bus_width = value @property def max_bus_width(self) -> float: """ :return: """ return self._max_bus_width @max_bus_width.setter def max_bus_width(self, value: float): self._max_bus_width = value @property def arrow_size(self) -> float: """ :return: """ return self._arrow_size @arrow_size.setter def arrow_size(self, value: float): self._arrow_size = value # palette property @property def palette(self) -> Colormaps: """ :return: """ return self._palette @palette.setter def palette(self, value: Colormaps): assert isinstance(value, Colormaps) self._palette = value # default_bus_voltage property @property def default_bus_voltage(self) -> float: """ :return: """ return self._default_bus_voltage @default_bus_voltage.setter def default_bus_voltage(self, value: float): self._default_bus_voltage = value @property def use_api_colors(self) -> bool: return self._use_api_colors @use_api_colors.setter def use_api_colors(self, value: bool): self._use_api_colors = value
[docs] def set_point(self, device: ALL_DEV_TYPES, location: Union[GraphicLocation, MapLocation]): """ :param device: :param location: :return: """ # check if the category exists ... d = self.data.get(str(device.device_type.value), None) if location.api_object is None: location.api_object = device if d is None: # the category does not exist, create it group = PointsGroup(name=str(device.device_type.value)) group.set_point(device, location) self.data[str(device.device_type.value)] = group else: # the category does exist, add point d.set_point(device, location) # the category, exists, just add
[docs] def delete_device(self, device: ALL_DEV_TYPES) -> Union[object, None]: """ :param device: :return: """ if device is not None: # check if the category exists ... d = self.data.get(str(device.device_type.value), None) if d: # the category does exist, delete_with_dialogue from it return d.delete_device(device=device) else: # not found so we're ok return None else: return None
[docs] def query_point(self, device: ALL_DEV_TYPES) -> Union[GraphicLocation, MapLocation, None]: """ :param device: :return: """ # check if the category exists ... group = self.data.get(str(device.device_type.value), None) if group is None: return None # the category did not exist else: # search for the device idtag and return the location, if not found return None return group.query_point(device)
[docs] def query_by_type(self, device_type: DeviceType) -> Union[PointsGroup, None]: """ Query diagram by device type :param device_type: DeviceType :return: PointsGroup """ # check if the category exists ... group = self.data.get(device_type.value, None) return group
[docs] def get_data_dict(self) -> Dict[str, Union[str, int, float, Dict[str, Union[GraphicLocation, MapLocation]]]]: """ get the properties dictionary to save :return: dictionary to serialize """ data = {category: group.get_dict() for category, group in self.data.items()} return {'type': self.diagram_type.value, 'idtag': self.idtag, 'name': self.name, "use_flow_based_width": self.use_flow_based_width, "min_branch_width": self.min_branch_width, "max_branch_width": self.max_branch_width, "min_bus_width": self.min_bus_width, "max_bus_width": self.max_bus_width, "arrow_size": self.arrow_size, "use_api_colors": self.use_api_colors, "palette": self.palette.value, "default_bus_voltage": self.default_bus_voltage, 'data': data}
[docs] def parse_data(self, data: Dict[str, Dict[str, Dict[str, Union[int, float, bool, List[Tuple[float, float]]]]]], obj_dict: Dict[str, Dict[str, ALL_DEV_TYPES]], logger: Logger): """ Parse file data ito this class :param data: json dictionary :param obj_dict: dictionary of circuit objects by type to fincd the api objects back from file loading :param logger: logger """ self.data = dict() self.name = data['name'] self.use_flow_based_width: bool = data.get("use_flow_based_width", False) self.min_branch_width: float = data.get("min_branch_width", 1) self.max_branch_width: float = data.get("max_branch_width", 5) self.min_bus_width: float = data.get("min_bus_width", 1) self.max_bus_width: float = data.get("max_bus_width", 20) self.arrow_size: float = data.get("arrow_size", 1) palette_name: str = data.get("palette", 'VeraGrid') if palette_name in [a.value for a in Colormaps]: self.palette = Colormaps(palette_name) else: self.palette = Colormaps.VeraGrid self.default_bus_voltage = data.get("default_bus_voltage", 10) self.use_api_colors = data.get("use_api_colors", False) if data['type'] == 'bus-branch': self.diagram_type = DiagramType.Schematic else: self.diagram_type = DiagramType(data['type']) for category, loc_dict in data['data'].items(): points_group = PointsGroup(name=category) points_group.parse_data(data=loc_dict, obj_dict=obj_dict.get(category, dict()), logger=logger, category=category) self.data[category] = points_group
[docs] def build_graph(self) -> Tuple[nx.MultiDiGraph, List[Bus | FluidNode | Transformer3W | TransformerNW]]: """ Returns a networkx MultiDiGraph object of the grid. return MultiDiGraph, List[BusGraphicObject """ graph = nx.MultiDiGraph() node_devices = list() # visible node-like diagram anchors # Add buses, cn, bus-bars -------------------------------------------------------------------------------------- node_count = 0 graph_node_dictionary = dict() for dev_tpe in [DeviceType.BusDevice, DeviceType.BusBarDevice]: device_groups = self.data.get(dev_tpe.value, None) if device_groups: for i, (idtag, location) in enumerate(device_groups.locations.items()): api_object: ALL_DEV_TYPES = location.api_object skip_internal_bus: bool = dev_tpe == DeviceType.BusDevice and api_object.graphic_type == BusGraphicType.Internal # Internal anchor buses such as Transformer3W.bus0 and TransformerNW.bus0 are not visible nodes in the # schematic. They must be skipped here so later remapping can bind their idtag to # the owning visible device instead of leaving an isolated dead node in the graph. if skip_internal_bus: skip_internal_bus = skip_internal_bus else: # Each node receives explicit electrical metadata so the GUI layout # can build stable hierarchical placements without re-inferring state. add_node_layout_data(graph=graph, node_index=node_count, api_object=api_object) graph_node_dictionary[idtag] = node_count node_devices.append(api_object) node_count += 1 # Add fluid nodes ---------------------------------------------------------------------------------------------- fluid_node_groups = self.data.get(DeviceType.FluidNodeDevice.value, None) if fluid_node_groups: for i, (idtag, location) in enumerate(fluid_node_groups.locations.items()): # Fluid nodes share the electrical layer of the bus they are attached to. add_node_layout_data(graph=graph, node_index=node_count, api_object=location.api_object) graph_node_dictionary[idtag] = node_count if location.api_object.bus is not None: # the electrical bus location is the same graph_node_dictionary[location.api_object.bus.idtag] = node_count node_devices.append(location.api_object) node_count += 1 # Add multi-winding transformers ------------------------------------------------------------------------------ tr3_groups = self.data.get(DeviceType.Transformer3WDevice.value, None) if tr3_groups: for i, (idtag, location) in enumerate(tr3_groups.locations.items()): # The internal transformer bus defines the voltage layer for the device symbol. add_node_layout_data(graph=graph, node_index=node_count, api_object=location.api_object) graph_node_dictionary[idtag] = node_count if location.api_object.bus0 is not None: # the electrical bus location is the same graph_node_dictionary[location.api_object.bus0.idtag] = node_count node_devices.append(location.api_object) node_count += 1 tr_nw_groups = self.data.get(DeviceType.TransformerNwDevice.value, None) if tr_nw_groups: for i, (idtag, location) in enumerate(tr_nw_groups.locations.items()): # The internal transformer bus defines the voltage layer for the device symbol. add_node_layout_data(graph=graph, node_index=node_count, api_object=location.api_object) graph_node_dictionary[idtag] = node_count if location.api_object.bus0 is not None: # the electrical bus location is the same graph_node_dictionary[location.api_object.bus0.idtag] = node_count node_devices.append(location.api_object) node_count += 1 # Add the electrical branches ---------------------------------------------------------------------------------- for dev_type in [DeviceType.LineDevice, DeviceType.Transformer2WDevice, DeviceType.WindingDevice, DeviceType.UpfcDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.bus_from.idtag, None) t = graph_node_dictionary.get(branch.bus_to.idtag, None) if f is not None and t is not None: impedance = branch.get_weight() if dev_type == DeviceType.Transformer2WDevice or dev_type == DeviceType.WindingDevice: impedance *= 1e-3 else: impedance = impedance add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=impedance) for dev_type in [DeviceType.DCLineDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.bus_from.idtag, None) t = graph_node_dictionary.get(branch.bus_to.idtag, None) if f is not None and t is not None: add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=branch.get_weight()) for dev_type in [DeviceType.HVDCLineDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.bus_from.idtag, None) t = graph_node_dictionary.get(branch.bus_to.idtag, None) if f is not None and t is not None: add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=max(branch.get_weight(), 0.01)) for dev_type in [DeviceType.VscDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.bus_from.idtag, None) t = graph_node_dictionary.get(branch.bus_to.idtag, None) if f is not None and t is not None: add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=max(branch.get_weight(), 0.01)) for dev_type in [DeviceType.SwitchDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.bus_from.idtag, None) t = graph_node_dictionary.get(branch.bus_to.idtag, None) if f is not None and t is not None: add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=max(branch.get_weight(), 0.001)) # Add fluid branches ------------------------------------------------------------------------------------------- for dev_type in [DeviceType.FluidPathDevice]: groups = self.data.get(dev_type.value, None) if groups: for i, (idtag, location) in enumerate(groups.locations.items()): branch = location.api_object f = graph_node_dictionary.get(branch.source.idtag, None) t = graph_node_dictionary.get(branch.target.idtag, None) if f is not None and t is not None: add_branch_layout_edge(graph=graph, from_idx=f, to_idx=t, branch=branch, dev_tpe=dev_type, raw_impedance=0.01) return graph, node_devices
[docs] def get_boundaries(self): """ Get the graphic representation boundaries :return: min_x, max_x, min_y, max_y """ min_x = sys.maxsize min_y = sys.maxsize max_x = -sys.maxsize max_y = -sys.maxsize # shrink selection only for tpe, group in self.data.items(): for key, location in group.locations.items(): x = location.x y = location.y max_x = max(max_x, x) min_x = min(min_x, x) max_y = max(max_y, y) min_y = min(min_y, y) return min_x, max_x, min_y, max_y
[docs] def set_size_constraints(self, use_flow_based_width: bool = False, min_branch_width: int = 5, max_branch_width=5, min_bus_width=20, max_bus_width=20, arrow_size=20): """ Set the size constraints :param use_flow_based_width: :param min_branch_width: :param max_branch_width: :param min_bus_width: :param max_bus_width: :param arrow_size: """ self.use_flow_based_width: bool = use_flow_based_width self.min_branch_width: float = min_branch_width self.max_branch_width: float = max_branch_width self.min_bus_width: float = min_bus_width self.max_bus_width: float = max_bus_width self.arrow_size = arrow_size
[docs] def copy_diagrams(diagrams: List[BaseDiagram], obj_dict: Dict[str, Dict[str, ALL_DEV_TYPES]] | None = None) -> List[BaseDiagram]: """ Copy diagrams while treating API objects as pointers. :param diagrams: Diagrams to copy. :param obj_dict: Optional target circuit object dictionary used to rebind pointers. :return: Copied diagrams. """ return [diagram.copy(obj_dict=obj_dict) for diagram in diagrams]