Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/power_grid_model_ds/_core/visualizer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ def get_app_layout(grid: Grid) -> html.Div:

initial_elements = filter_out_appliances(viz_elements_dict.values())

# Remove associated_ids from initial element data.
# They will be added and used after migrating to using grid object for callbacks
for element in initial_elements:
element["data"].pop("associated_ids", None)

cytoscape_html = get_cytoscape_html(layout, initial_elements, grid.source.size != 0)

return html.Div(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@


from typing import Any
from venv import logger

from dash import Input, Output, callback, dash_table
from dash import Input, Output, callback, dash_table, html

from power_grid_model_ds._core.model.grids.base import Grid
from power_grid_model_ds._core.visualizer.layout.selection_output import (
SELECTION_OUTPUT_HTML,
)
from power_grid_model_ds._core.visualizer.parsing_utils import viz_id_to_pgm_id
from power_grid_model_ds._core.visualizer.server_state import get_grid
from power_grid_model_ds.arrays import IdArray

Expand All @@ -23,33 +23,51 @@
)
def display_selected_element(node_data: list[dict[str, Any]], edge_data: list[dict[str, Any]]):
"""Display the tapped edge data."""
# 0th element means data for only a single selection is shown
if node_data:
selected_data = node_data.pop()
elif edge_data:
selected_data = edge_data.pop()
else:
if not node_data and not edge_data:
return SELECTION_OUTPUT_HTML.children

group = selected_data["group"]
pgm_id = viz_id_to_pgm_id(selected_data["id"])
# Extract the associated pgm_ids for all selected nodes and edges, grouped by component type
items_to_visualize: dict[str, list[int]] = {}
for node_edge_data in [node_data, edge_data]:
if node_edge_data is None:
continue
for elm in node_edge_data:
for group, pgm_ids in elm["associated_ids"].items():
if group not in items_to_visualize:
items_to_visualize[group] = []
items_to_visualize[group].extend(pgm_ids)

grid: Grid = get_grid()
array_data = getattr(grid, group).get(id=pgm_id)

return _to_data_table(array_data)
tables: list[html.H5 | html.Div] = []
for comp_type, ids in items_to_visualize.items():
# Select unique as multiple selected elements can be connected to the same component
# (e.g. appliance and node selected together, branch3 edges selected together)
unique_ids = list(set(ids))
data = getattr(grid, comp_type).filter(id=unique_ids)
tables.append(html.H5(comp_type, style={"marginTop": "15px", "textAlign": "left"}))
tables.append(_to_data_table(data))

return html.Div(children=tables, style={"overflowX": "scroll", "margin": "10px"}).children


def _to_data_table(array_data: IdArray):
array_data_dict = {}
for column in array_data.columns:
array_data_dict[column] = array_data[column].item()
list_array_data = []
for entry in array_data:
record_dict = {}
for col in array_data.columns:
if entry[col].ndim == 1:
record_dict[col] = entry[col].item()
else:
logger.warning(f"Column '{col}' in group '{array_data.name}' is not 1-dimensional. Skipping search.")

list_array_data.append(record_dict)

# ignore[attr-defined] added for https://github.com/plotly/dash/issues/3226
data_table = dash_table.DataTable( # type: ignore[attr-defined]
data=[array_data_dict],
columns=[{"name": key, "id": key} for key in array_data_dict],
data=list_array_data,
columns=[{"name": key, "id": key} for key in list_array_data[0]],
editable=False,
fill_width=False,
)
return data_table
return html.Div(data_table)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def get_cytoscape_html(layout: LayoutOptions, elements: list[dict[str, Any]], so
style=_CYTO_INNER_STYLE,
elements=elements,
stylesheet=DEFAULT_STYLESHEET,
zoom=1.0, # Default zoom level
minZoom=0.05,
maxZoom=3.0,
boxSelectionEnabled=True,
wheelSensitivity=0.2, # Smooth zooming
),
style=_CYTO_OUTER_STYLE,
)
97 changes: 96 additions & 1 deletion src/power_grid_model_ds/_core/visualizer/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from power_grid_model_ds._core.model.grids.base import Grid
from power_grid_model_ds._core.visualizer.parsing_utils import (
append_component_list_parsed_elements,
map_appliance_to_nodes,
)
from power_grid_model_ds._core.visualizer.styling_classification import (
StyleClass,
Expand All @@ -19,13 +20,23 @@
from power_grid_model_ds._core.visualizer.typing import (
ComponentTypeAppliance,
ComponentTypeBranch,
ComponentTypeFlowSensor,
VizToComponentElements,
)
from power_grid_model_ds.arrays import (
ApplianceArray,
AsymCurrentSensorArray,
AsymPowerSensorArray,
AsymVoltageSensorArray,
Branch3Array,
BranchArray,
FaultArray,
NodeArray,
SymCurrentSensorArray,
SymPowerSensorArray,
SymVoltageSensorArray,
TransformerTapRegulatorArray,
VoltageRegulatorArray,
)

_NODE_BRANCH_TERMINAL_TYPE = [
Expand Down Expand Up @@ -78,6 +89,20 @@ def parse_element_data(grid: Grid) -> VizToComponentElements:
_parse_appliances(elements, grid.asym_gen, ComponentType.asym_gen)
_parse_appliances(elements, grid.shunt, ComponentType.shunt)

# Parse sensors
appliance_to_node = map_appliance_to_nodes(grid)
_parse_flow_sensors(elements, grid.sym_power_sensor, ComponentType.sym_power_sensor, appliance_to_node)
_parse_flow_sensors(elements, grid.asym_power_sensor, ComponentType.asym_power_sensor, appliance_to_node)
_parse_flow_sensors(elements, grid.sym_current_sensor, ComponentType.sym_current_sensor, appliance_to_node)
_parse_flow_sensors(elements, grid.asym_current_sensor, ComponentType.asym_current_sensor, appliance_to_node)
_parse_voltage_sensors(elements, grid.sym_voltage_sensor, ComponentType.sym_voltage_sensor)
_parse_voltage_sensors(elements, grid.asym_voltage_sensor, ComponentType.asym_voltage_sensor)
_parse_voltage_regulators(elements, grid.voltage_regulator)
_parse_faults(elements, grid.fault)

# Parse regulators
_parse_transformer_tap_regulators(elements, grid.transformer_tap_regulator)

return elements


Expand Down Expand Up @@ -174,7 +199,6 @@ def _parse_appliances(elements: VizToComponentElements, array: ApplianceArray, g
"group": group.value,
"associated_ids": {group.value: [appliance.id.item()]},
},
"selectable": False,
"classes": get_appliance_edge_classification(appliance, group),
}

Expand All @@ -185,3 +209,74 @@ def _parse_appliances(elements: VizToComponentElements, array: ApplianceArray, g
} # invert y-axis for visualization

append_component_list_parsed_elements(elements, appliance.id.item(), node_id_str, group.value)


def _parse_flow_sensors(
elements: VizToComponentElements,
array: SymPowerSensorArray | SymCurrentSensorArray | AsymPowerSensorArray | AsymCurrentSensorArray,
group: ComponentTypeFlowSensor,
appliance_to_node: dict[str, str],
):
"""Parse power sensors and return appliance-to-power-sensor mapping."""
for power_sensor in array:
measured_object_id_str = str(power_sensor.measured_object.item())
measured_terminal_type = power_sensor.measured_terminal_type.item()

if measured_terminal_type in _NODE_BRANCH_TERMINAL_TYPE:
mapping_id_strs = [measured_object_id_str]
elif measured_terminal_type in _BRANCH3_TERMINAL_TYPE:
mapping_id_strs = [f"{measured_object_id_str}_{count}" for count in range(3)]
elif measured_terminal_type in _APPLIANCE_TERMINAL_TYPE:
mapping_id_strs = [measured_object_id_str]
# Map appliance to both appliance and its node as both can be unvisualized
if measured_object_id_str in appliance_to_node:
mapping_id_strs.append(appliance_to_node[measured_object_id_str])
else:
raise ValueError(f"Unknown measured_terminal_type: {measured_terminal_type}")

for id_str in mapping_id_strs:
append_component_list_parsed_elements(elements, power_sensor.id.item(), id_str, group.value)


def _parse_voltage_sensors(
elements: VizToComponentElements,
array: SymVoltageSensorArray | AsymVoltageSensorArray,
group: Literal[ComponentType.sym_voltage_sensor, ComponentType.asym_voltage_sensor],
):
"""Parse voltage sensors and associate them with nodes."""
for voltage_sensor in array:
node_id_str = str(voltage_sensor.measured_object.item())
append_component_list_parsed_elements(elements, voltage_sensor.id.item(), node_id_str, group.value)


def _parse_transformer_tap_regulators(elements: VizToComponentElements, array: TransformerTapRegulatorArray):
"""Parse transformer tap regulators and associate them with transformers."""
for tap_regulator in array:
regulated_object_str = str(tap_regulator.regulated_object.item())
if regulated_object_str in elements:
mapping_id_strs = [regulated_object_str]
else:
mapping_id_strs = [
f"{regulated_object_str}_{count}" for count in range(3) if f"{regulated_object_str}_{count}" in elements
]

for id_str in mapping_id_strs:
append_component_list_parsed_elements(
elements, tap_regulator.id.item(), id_str, ComponentType.transformer_tap_regulator.value
)


def _parse_voltage_regulators(elements: VizToComponentElements, array: VoltageRegulatorArray):
"""Parse voltage regulators and associate them with nodes."""
for voltage_regulator in array:
regulated_object_str = str(voltage_regulator.regulated_object.item())
append_component_list_parsed_elements(
elements, voltage_regulator.id.item(), regulated_object_str, ComponentType.voltage_regulator.value
)


def _parse_faults(elements: VizToComponentElements, array: FaultArray):
"""Parse faults and associate them with nodes."""
for fault in array:
fault_object_str = str(fault.fault_object.item())
append_component_list_parsed_elements(elements, fault.id.item(), fault_object_str, ComponentType.fault.value)
7 changes: 4 additions & 3 deletions tests/unit/visualizer/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_element_selection_callback():

server_state.set_grid(grid)

node_data = [{"id": "1", "u_rated": 100.0, "group": "node"}]
node_data = [{"id": "1", "group": "node", "associated_ids": {"node": [1]}}]
edge_data = []

result = display_selected_element(node_data, edge_data)
Expand All @@ -96,8 +96,9 @@ def test_element_selection_callback():
],
editable=False,
)
assert result.data == expected.data
assert result.columns == expected.columns
assert result[0].children == "node"
assert result[1].children.data == expected.data
assert result[1].children.columns == expected.columns


def test_display_selected_element_none():
Expand Down
Loading
Loading