causality inference done
This commit is contained in:
@@ -37,16 +37,49 @@ def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[st
|
||||
|
||||
id_list = build_id_list(component)
|
||||
|
||||
# Check fixed causality
|
||||
for connection in graph.get("connections", []):
|
||||
if isinstance(connection, dict) and connection.get("type") == "power":
|
||||
source, target = get_ports_from_bond(connection, id_list)
|
||||
log.info(source)
|
||||
log.info(target)
|
||||
if source.get('causality', 'indifferent') == 'fixed effort out':
|
||||
connection['causality'] = 'target'
|
||||
elif source.get('causality', 'indifferent') == 'fixed flow out':
|
||||
connection['causality'] = 'source'
|
||||
# Fixed causalities
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'fixed effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'fixed flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
|
||||
# Preferred causalities
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if is_port_fully_assigned(block, port, graph):
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'preferred effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'preferred flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
|
||||
|
||||
|
||||
# Soft choices
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if is_port_fully_assigned(block, port, graph):
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'likes effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'likes flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') in ['indifferent', 'single flow in', 'single effort in']:
|
||||
# Force an arbitrary assignment on the first unassigned bond connected to this port
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
|
||||
# Last check
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power' and con.get('causality', 'none') == 'none':
|
||||
raise ValueError("System under-constrained: unresolved causal loops or disconnected elements remain")
|
||||
|
||||
for block in graph.get("blocks", []):
|
||||
if isinstance(block, dict):
|
||||
@@ -104,31 +137,167 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
||||
visit(graph)
|
||||
return id_list
|
||||
|
||||
def get_ports_from_bond(bond: dict[str, Any], id_list: dict[str, Any]) -> tuple[dict[str,Any], dict[str,Any]]:
|
||||
source = bond.get('source', None)
|
||||
target = bond.get('target', None)
|
||||
if source is None or target is None:
|
||||
raise ValueError("Source and Target of a power bond cannot be None")
|
||||
|
||||
source_component = id_list.get(source.get('block'), None)
|
||||
target_component = id_list.get(target.get('block'), None)
|
||||
if source_component is None or target_component is None:
|
||||
raise ValueError("Source or Target blocks not found")
|
||||
|
||||
def _find_port(ports: list[dict[str, Any]], port: str) -> dict[str, Any] | None:
|
||||
for pi in ports:
|
||||
if pi.get('id', '') == port:
|
||||
return pi
|
||||
return None
|
||||
def is_port_fully_assigned(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> bool:
|
||||
block_id = block.get('id')
|
||||
port_id = port.get('id')
|
||||
for connection in graph.get('connections', []):
|
||||
if connection.get('type') != 'power':
|
||||
continue
|
||||
source = connection.get('source', {})
|
||||
target = connection.get('target', {})
|
||||
if ((source.get('block') == block_id and source.get('port') == port_id)
|
||||
or (target.get('block') == block_id and target.get('port') == port_id)):
|
||||
if connection.get('causality', 'none') == 'none':
|
||||
return False
|
||||
return True
|
||||
|
||||
source_port = _find_port(source_component.get('interface', {}).get('ports', []), source.get('port'))
|
||||
target_port = _find_port(target_component.get('interface', {}).get('ports', []), target.get('port'))
|
||||
if source_port is None or target_port is None:
|
||||
raise ValueError("Source or Target port not found")
|
||||
def propagate_from_port(block: dict[str, Any], port: dict[str, Any], desired_causality: str, graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# find all connection on this specific port
|
||||
attached_connections = []
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power':
|
||||
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
if not attached_connections:
|
||||
# unconnected port
|
||||
return
|
||||
|
||||
if source_port.get('type') != 'power':
|
||||
raise ValueError("Source port is not a power port")
|
||||
if target_port.get('type') != 'power':
|
||||
raise ValueError("Target port is not a power port")
|
||||
# find first unassigned connection on this port
|
||||
target_connection = None
|
||||
for con in attached_connections:
|
||||
if con.get('causality', None) == 'none':
|
||||
target_connection = con
|
||||
break
|
||||
if target_connection is None:
|
||||
# Nothing left to resolve
|
||||
return
|
||||
|
||||
# Update
|
||||
if target_connection.get('source').get('block') == block.get('id'):
|
||||
target_connection['causality'] = 'target' if (desired_causality == 'effort out') else 'source'
|
||||
else:
|
||||
target_connection['causality'] = 'source' if (desired_causality == 'effort out') else 'target'
|
||||
|
||||
propagate_to_neighbor_from_connection(block, target_connection, graph, id_list)
|
||||
|
||||
def evaluate_junction_constraints(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# find all connection on this specific port
|
||||
attached_connections = []
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power':
|
||||
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
if not attached_connections:
|
||||
# unconnected port
|
||||
return
|
||||
|
||||
return source_port, target_port
|
||||
efforts_in = 0
|
||||
efforts_out = 0
|
||||
unnassigned_conns = []
|
||||
# Count current states relative to the junction port
|
||||
for con in attached_connections:
|
||||
if con.get('causality', 'none') == 'none':
|
||||
unnassigned_conns.append(con)
|
||||
continue
|
||||
|
||||
is_source = (con.get('source').get('block') == block.get('id'))
|
||||
if (is_source and con.get('causality', 'none')=='source') or (not is_source and con.get('causality', 'none')=='target'):
|
||||
efforts_in += 1
|
||||
else:
|
||||
efforts_out += 1
|
||||
|
||||
# Single effort in
|
||||
if port.get('causality', 'indifferent') == 'single effort in':
|
||||
# ERROR CHECK: Multiple sources/blocks trying to claim effort control on a 0-junction
|
||||
if efforts_in>1:
|
||||
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating effort to 0 junction: {block.get('name')}")
|
||||
|
||||
# 1 effort is coming in so all other ports must be effort out
|
||||
if efforts_in == 1 and len(unnassigned_conns) > 0:
|
||||
for con in unnassigned_conns:
|
||||
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# No effort is coming in yet and one left so must be effort in
|
||||
if efforts_in == 0 and len(unnassigned_conns) == 1:
|
||||
con = unnassigned_conns[0]
|
||||
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# Single flow in
|
||||
elif port.get('causality', 'indifferent') == 'single flow in':
|
||||
# ERROR CHECK: Multiple sources/blocks trying to claim flow control on a 1-junction
|
||||
if efforts_out>1:
|
||||
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating flow to 1 junction: {block.get('name')}")
|
||||
|
||||
# 1 flow is coming in so all other ports must be flow out
|
||||
if efforts_out == 1 and len(unnassigned_conns) > 0:
|
||||
for con in unnassigned_conns:
|
||||
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# No flow is coming in yet and one left so must be flow in
|
||||
if efforts_out == 0 and len(unnassigned_conns) == 1:
|
||||
con = unnassigned_conns[0]
|
||||
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
def propagate_to_neighbor_from_connection(block: dict[str, Any], connection: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# Get the neighbor block and port on the other side of the bond
|
||||
neighbor_id = connection.get('target').get('block') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('block')
|
||||
neighbor_port_id = connection.get('target').get('port') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('port')
|
||||
neighbor_block = id_list.get(neighbor_id, None)
|
||||
if neighbor_block is None:
|
||||
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||
neighbor_ports = neighbor_block.get('interface').get('ports', [])
|
||||
neighbor_port = None
|
||||
for p in neighbor_ports:
|
||||
if p.get('id') == neighbor_port_id:
|
||||
neighbor_port = p
|
||||
break
|
||||
if neighbor_port is None:
|
||||
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||
|
||||
# Trigger evaluation on the neighbor
|
||||
if neighbor_port.get('causality', 'indifferent') in ['single effort in', 'single flow in']:
|
||||
evaluate_junction_constraints(neighbor_block, neighbor_port, graph, id_list)
|
||||
else:
|
||||
verify_component_compatibility(neighbor_block, neighbor_port, graph)
|
||||
|
||||
def verify_component_compatibility(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> None:
|
||||
# Find the connection we are evaluating
|
||||
conn = None
|
||||
for c in graph.get('connections', []):
|
||||
if (c.get('source').get('block') == block.get('id') and c.get('source').get('port') == port.get('id')) or (c.get('target').get('block') == block.get('id') and c.get('target').get('port') == port.get('id')):
|
||||
if c.get('causality', 'none') != 'none':
|
||||
conn = c
|
||||
break
|
||||
|
||||
if conn is None:
|
||||
return
|
||||
|
||||
# Figure out what causality was pushed onto this port from the outside world
|
||||
state = conn.get('causality', 'none')
|
||||
is_source = conn.get('source', {}).get('block') == block.get('id')
|
||||
effort_out = (is_source and state == 'target') or (not is_source and state == 'source')
|
||||
port_causality = port.get('causality', 'indifferent')
|
||||
|
||||
if port_causality == 'fixed effort out' and not effort_out:
|
||||
raise ValueError(f"Critical source conflict: Fixed effort source '{block.get('name')}' forced into an input state.")
|
||||
if port_causality == 'fixed flow out' and effort_out:
|
||||
raise ValueError(f"Critical source conflict: Fixed flow source '{block.get('name')}' forced into an input state.")
|
||||
|
||||
if port_causality == 'preferred effort out' and not effort_out:
|
||||
conn['causality'] = f'warn_{state}'
|
||||
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||
if port_causality == 'preferred flow out' and effort_out:
|
||||
conn['causality'] = f'warn_{state}'
|
||||
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||
|
||||
@@ -6,7 +6,11 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
from bedit.core.power_domains import POWER_CAUSALITIES, POWER_DOMAINS
|
||||
from bedit.core.power_domains import (
|
||||
MULTI_CONNECTION_POWER_CAUSALITIES,
|
||||
POWER_CAUSALITIES,
|
||||
POWER_DOMAINS,
|
||||
)
|
||||
|
||||
|
||||
def _dimensions(data: Any, subject: str) -> tuple[int, int]:
|
||||
@@ -547,6 +551,14 @@ class GraphDocument:
|
||||
raise ValueError(f"Power port {port.name!r} has an unknown domain")
|
||||
if port.causality not in POWER_CAUSALITIES:
|
||||
raise ValueError(f"Power port {port.name!r} has invalid causality")
|
||||
if (
|
||||
port.causality in MULTI_CONNECTION_POWER_CAUSALITIES
|
||||
and not port.allows_multiple_connections
|
||||
):
|
||||
raise ValueError(
|
||||
f"Power port {port.name!r} requires multiple connections "
|
||||
f"for causality {port.causality!r}"
|
||||
)
|
||||
if not port.value_type.strip():
|
||||
raise ValueError(f"Port {port.name!r} has no value type")
|
||||
if port.rows < 1 or port.columns < 1:
|
||||
|
||||
@@ -20,7 +20,7 @@ def power_domain(domain_id: str) -> PowerDomain:
|
||||
return next((domain for domain in POWER_DOMAINS if domain.id == domain_id), POWER_DOMAINS[0])
|
||||
|
||||
|
||||
POWER_CAUSALITIES = (
|
||||
STANDARD_POWER_CAUSALITIES = (
|
||||
"fixed flow out",
|
||||
"fixed effort out",
|
||||
"preferred flow out",
|
||||
@@ -29,3 +29,21 @@ POWER_CAUSALITIES = (
|
||||
"likes effort out",
|
||||
"indifferent",
|
||||
)
|
||||
|
||||
MULTI_CONNECTION_POWER_CAUSALITIES = (
|
||||
"single effort in",
|
||||
"single flow in",
|
||||
)
|
||||
|
||||
POWER_CAUSALITIES = (
|
||||
*STANDARD_POWER_CAUSALITIES,
|
||||
*MULTI_CONNECTION_POWER_CAUSALITIES,
|
||||
)
|
||||
|
||||
|
||||
def power_causalities(allows_multiple_connections: bool = False) -> tuple[str, ...]:
|
||||
"""Return the causalities available for one power port configuration."""
|
||||
|
||||
if allows_multiple_connections:
|
||||
return POWER_CAUSALITIES
|
||||
return STANDARD_POWER_CAUSALITIES
|
||||
|
||||
@@ -48,6 +48,8 @@ class Simulation:
|
||||
) -> None:
|
||||
"""Compose and retain the active graph's Modelica representation."""
|
||||
|
||||
return
|
||||
|
||||
self.model_path = None
|
||||
self.compose_source(graph)
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ from bedit.core.bond_graph import infer_causality
|
||||
from bedit.core.simulation import Simulation
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
from bedit.core.serializer import DocumentSerializer
|
||||
from bedit.gui.preferences import application_settings
|
||||
|
||||
|
||||
class DocumentController(QObject):
|
||||
@@ -479,7 +480,9 @@ class DocumentController(QObject):
|
||||
error_callback,
|
||||
)
|
||||
|
||||
def _infer_active_graph_causality(self, component: Component) -> None:
|
||||
def _infer_active_graph_causality(
|
||||
self, component: Component, *, emit_reset: bool = True
|
||||
) -> None:
|
||||
"""Infer causality and copy the derived values into the live model."""
|
||||
|
||||
inferred = infer_causality(component.to_dict())
|
||||
@@ -505,7 +508,8 @@ class DocumentController(QObject):
|
||||
if changed:
|
||||
if self.document is not None:
|
||||
self.document.validate()
|
||||
self.documentReset.emit()
|
||||
if emit_reset:
|
||||
self.documentReset.emit()
|
||||
|
||||
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
||||
item = (
|
||||
@@ -1175,6 +1179,10 @@ class DocumentController(QObject):
|
||||
|
||||
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
||||
self._graph_for(owner_id).connections[connection.id] = connection
|
||||
if self.document is not None and self._infer_causality_on_connection_change():
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is not None:
|
||||
self._infer_active_graph_causality(owner, emit_reset=False)
|
||||
if owner_id == self.active_component_id:
|
||||
self.connectionAdded.emit(connection.id)
|
||||
self.documentReset.emit()
|
||||
@@ -1211,10 +1219,20 @@ class DocumentController(QObject):
|
||||
|
||||
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||
self._graph_for(owner_id).connections.pop(connection_id, None)
|
||||
if self.document is not None and self._infer_causality_on_connection_change():
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is not None:
|
||||
self._infer_active_graph_causality(owner, emit_reset=False)
|
||||
if owner_id == self.active_component_id:
|
||||
self.connectionRemoved.emit(connection_id)
|
||||
self.documentReset.emit()
|
||||
|
||||
@staticmethod
|
||||
def _infer_causality_on_connection_change() -> bool:
|
||||
return application_settings().value(
|
||||
"bondGraph/inferCausalityOnConnectionChange", True, type=bool
|
||||
)
|
||||
|
||||
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
||||
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
||||
if owner_id == self.active_component_id:
|
||||
|
||||
@@ -6,7 +6,7 @@ from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessa
|
||||
|
||||
from bedit.core.model import Component, Port
|
||||
from bedit.core.physical_types import QUANTITIES, UNITS
|
||||
from bedit.core.power_domains import POWER_CAUSALITIES, POWER_DOMAINS, power_domain
|
||||
from bedit.core.power_domains import POWER_DOMAINS, power_causalities, power_domain
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
||||
|
||||
@@ -37,14 +37,16 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.orientationCombo.setItemData(2, "indifferent")
|
||||
for domain in POWER_DOMAINS:
|
||||
self.ui.domainCombo.addItem(domain.display_name, domain.id)
|
||||
self.ui.causalityCombo.addItems(POWER_CAUSALITIES)
|
||||
self._refresh_causality_options()
|
||||
self.ui.portList.currentRowChanged.connect(self._load_current)
|
||||
self.ui.addPortButton.clicked.connect(self.add_port)
|
||||
self.ui.removePortButton.clicked.connect(self.remove_port)
|
||||
self.ui.nameEdit.textEdited.connect(self._store_current)
|
||||
self.ui.typeCombo.currentIndexChanged.connect(self._port_type_changed)
|
||||
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
|
||||
self.ui.multipleConnectionsCheckBox.toggled.connect(self._store_current)
|
||||
self.ui.multipleConnectionsCheckBox.toggled.connect(
|
||||
self._multiple_connections_changed
|
||||
)
|
||||
self.ui.valueTypeCombo.currentTextChanged.connect(self._store_current)
|
||||
self.ui.quantityCombo.currentTextChanged.connect(self._store_current)
|
||||
self.ui.unitCombo.currentTextChanged.connect(self._store_current)
|
||||
@@ -96,6 +98,11 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.columnsSpin.setValue(port.columns)
|
||||
self.ui.descriptionEdit.setPlainText(port.description)
|
||||
self.ui.domainCombo.setCurrentIndex(self.ui.domainCombo.findData(port.domain))
|
||||
self._refresh_causality_options(
|
||||
port.causality,
|
||||
port_type=port.type,
|
||||
allows_multiple_connections=port.allows_multiple_connections,
|
||||
)
|
||||
self.ui.causalityCombo.setCurrentText(port.causality)
|
||||
self.ui.powerDescriptionEdit.setPlainText(port.description)
|
||||
self._show_type_options(port.type)
|
||||
@@ -120,9 +127,36 @@ class PortOptionsDialog(QDialog):
|
||||
indifferent_item.setEnabled(port_type == "power")
|
||||
if port_type != "power" and self.ui.orientationCombo.currentData() == "indifferent":
|
||||
self.ui.orientationCombo.setCurrentIndex(0)
|
||||
self._refresh_causality_options(port_type=port_type)
|
||||
self._show_type_options(port_type)
|
||||
self._store_current()
|
||||
|
||||
def _multiple_connections_changed(self) -> None:
|
||||
self._refresh_causality_options()
|
||||
self._store_current()
|
||||
|
||||
def _refresh_causality_options(
|
||||
self,
|
||||
selected: str | None = None,
|
||||
*,
|
||||
port_type: str | None = None,
|
||||
allows_multiple_connections: bool | None = None,
|
||||
) -> None:
|
||||
selected = selected or self.ui.causalityCombo.currentText() or "indifferent"
|
||||
port_type = port_type or self.ui.typeCombo.currentData()
|
||||
if allows_multiple_connections is None:
|
||||
allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
||||
choices = power_causalities(
|
||||
port_type == "power" and allows_multiple_connections
|
||||
)
|
||||
self.ui.causalityCombo.blockSignals(True)
|
||||
self.ui.causalityCombo.clear()
|
||||
self.ui.causalityCombo.addItems(choices)
|
||||
self.ui.causalityCombo.setCurrentText(
|
||||
selected if selected in choices else "indifferent"
|
||||
)
|
||||
self.ui.causalityCombo.blockSignals(False)
|
||||
|
||||
def _show_type_options(self, port_type: str) -> None:
|
||||
self.ui.orientationCombo.model().item(2).setEnabled(port_type == "power")
|
||||
page = {
|
||||
|
||||
@@ -47,6 +47,9 @@ class SettingsDialog(QDialog):
|
||||
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
||||
self.ui.graphSnapSpinBox.setValue(self.graph_snap_size(self.settings))
|
||||
self.ui.iconGridSpinBox.setValue(self.icon_grid_size(self.settings))
|
||||
self.ui.automaticCausalityCheckBox.setChecked(
|
||||
self.infer_causality_on_connection_change(self.settings)
|
||||
)
|
||||
self.ui.openModelicaPathEdit.setText(self.openmodelica_path(self.settings))
|
||||
self._load_syntax_styles()
|
||||
self._update_remove_button()
|
||||
@@ -132,6 +135,15 @@ class SettingsDialog(QDialog):
|
||||
settings = settings if settings is not None else application_settings()
|
||||
return settings.value("grid/iconSize", 8, type=int)
|
||||
|
||||
@staticmethod
|
||||
def infer_causality_on_connection_change(
|
||||
settings: QSettings | None = None,
|
||||
) -> bool:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
return settings.value(
|
||||
"bondGraph/inferCausalityOnConnectionChange", True, type=bool
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def openmodelica_path(settings: QSettings | None = None) -> str:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
@@ -197,6 +209,10 @@ class SettingsDialog(QDialog):
|
||||
self.settings.setValue("grid/graphSize", self.ui.graphGridSpinBox.value())
|
||||
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
||||
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
||||
self.settings.setValue(
|
||||
"bondGraph/inferCausalityOnConnectionChange",
|
||||
self.ui.automaticCausalityCheckBox.isChecked(),
|
||||
)
|
||||
self.settings.setValue(
|
||||
"simulation/openModelicaPath",
|
||||
self.ui.openModelicaPathEdit.text().strip(),
|
||||
|
||||
@@ -15,12 +15,12 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
|
||||
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
|
||||
QHeaderView, QLabel, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem,
|
||||
QSpinBox, QTabWidget, QTableWidget, QTableWidgetItem,
|
||||
QVBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
|
||||
QDialog, QDialogButtonBox, QFormLayout, QGroupBox,
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
||||
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSpinBox, QTabWidget, QTableWidget,
|
||||
QTableWidgetItem, QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_SettingsDialog(object):
|
||||
def setupUi(self, SettingsDialog):
|
||||
@@ -104,6 +104,34 @@ class Ui_SettingsDialog(object):
|
||||
self.generalLayout.addItem(self.generalSpacer)
|
||||
|
||||
self.settingsTabs.addTab(self.generalTab, "")
|
||||
self.bondGraphTab = QWidget()
|
||||
self.bondGraphTab.setObjectName(u"bondGraphTab")
|
||||
self.bondGraphLayout = QVBoxLayout(self.bondGraphTab)
|
||||
self.bondGraphLayout.setObjectName(u"bondGraphLayout")
|
||||
self.causalityGroupBox = QGroupBox(self.bondGraphTab)
|
||||
self.causalityGroupBox.setObjectName(u"causalityGroupBox")
|
||||
self.causalityLayout = QVBoxLayout(self.causalityGroupBox)
|
||||
self.causalityLayout.setObjectName(u"causalityLayout")
|
||||
self.automaticCausalityCheckBox = QCheckBox(self.causalityGroupBox)
|
||||
self.automaticCausalityCheckBox.setObjectName(u"automaticCausalityCheckBox")
|
||||
self.automaticCausalityCheckBox.setChecked(True)
|
||||
|
||||
self.causalityLayout.addWidget(self.automaticCausalityCheckBox)
|
||||
|
||||
self.automaticCausalityHintLabel = QLabel(self.causalityGroupBox)
|
||||
self.automaticCausalityHintLabel.setObjectName(u"automaticCausalityHintLabel")
|
||||
self.automaticCausalityHintLabel.setWordWrap(True)
|
||||
|
||||
self.causalityLayout.addWidget(self.automaticCausalityHintLabel)
|
||||
|
||||
|
||||
self.bondGraphLayout.addWidget(self.causalityGroupBox)
|
||||
|
||||
self.bondGraphSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.bondGraphLayout.addItem(self.bondGraphSpacer)
|
||||
|
||||
self.settingsTabs.addTab(self.bondGraphTab, "")
|
||||
self.simulationTab = QWidget()
|
||||
self.simulationTab.setObjectName(u"simulationTab")
|
||||
self.simulationTabLayout = QVBoxLayout(self.simulationTab)
|
||||
@@ -247,6 +275,10 @@ class Ui_SettingsDialog(object):
|
||||
self.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None))
|
||||
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
|
||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
||||
self.causalityGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Causality", None))
|
||||
self.automaticCausalityCheckBox.setText(QCoreApplication.translate("SettingsDialog", u"Infer causality when connections are added or removed", None))
|
||||
self.automaticCausalityHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"When disabled, displayed causalities are updated only when compiling, exporting, or running the model.", None))
|
||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.bondGraphTab), QCoreApplication.translate("SettingsDialog", u"Bond graph", None))
|
||||
self.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None))
|
||||
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None))
|
||||
self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None))
|
||||
|
||||
@@ -27,7 +27,7 @@ class ConnectionStyle:
|
||||
# This is the intentional code-level styling point for every port/connection type.
|
||||
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
||||
"signal": ConnectionStyle(),
|
||||
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000"),
|
||||
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000", arrow_size=20.0),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -397,6 +397,19 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
self.name_label: NameLabelItem | None = None
|
||||
self.sync_name_label(connection)
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
"""Include custom arrowheads and causality marks in Qt's repaint area."""
|
||||
|
||||
decoration_margin = (
|
||||
max(self.style.arrow_size, 6.0) + self.style.selected_width / 2 + 1.0
|
||||
)
|
||||
return super().boundingRect().adjusted(
|
||||
-decoration_margin,
|
||||
-decoration_margin,
|
||||
decoration_margin,
|
||||
decoration_margin,
|
||||
)
|
||||
|
||||
def sync_name_label(self, connection: Connection) -> None:
|
||||
visible = bool(connection.properties.get("showName", False))
|
||||
if not visible:
|
||||
|
||||
Reference in New Issue
Block a user