causality inference done
This commit is contained in:
@@ -62,7 +62,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
- Port types are registered in `core/port_types.py`. Only compatible types may be
|
- Port types are registered in `core/port_types.py`. Only compatible types may be
|
||||||
connected. Signal ports connect by type; power ports additionally require the
|
connected. Signal ports connect by type; power ports additionally require the
|
||||||
same domain. Editable power domains and causalities live in
|
same domain. Editable power domains and causalities live in
|
||||||
`core/power_domains.py`.
|
`core/power_domains.py`. The `single effort in` and `single flow in`
|
||||||
|
causalities are available only on power ports that allow multiple connections.
|
||||||
- Power ports may use `indifferent` orientation and can act as either connection
|
- Power ports may use `indifferent` orientation and can act as either connection
|
||||||
endpoint. For two indifferent ports, click order determines source/arrow
|
endpoint. For two indifferent ports, click order determines source/arrow
|
||||||
direction; otherwise output/input semantics determine direction.
|
direction; otherwise output/input semantics determine direction.
|
||||||
|
|||||||
@@ -37,16 +37,49 @@ def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[st
|
|||||||
|
|
||||||
id_list = build_id_list(component)
|
id_list = build_id_list(component)
|
||||||
|
|
||||||
# Check fixed causality
|
# Fixed causalities
|
||||||
for connection in graph.get("connections", []):
|
for block in graph.get("blocks", []):
|
||||||
if isinstance(connection, dict) and connection.get("type") == "power":
|
for port in block.get('interface', {}).get('ports', []):
|
||||||
source, target = get_ports_from_bond(connection, id_list)
|
if port.get('type', '') != 'power':
|
||||||
log.info(source)
|
continue
|
||||||
log.info(target)
|
if port.get('causality', 'indifferent') == 'fixed effort out':
|
||||||
if source.get('causality', 'indifferent') == 'fixed effort out':
|
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||||
connection['causality'] = 'target'
|
elif port.get('causality', 'indifferent') == 'fixed flow out':
|
||||||
elif source.get('causality', 'indifferent') == 'fixed flow out':
|
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||||
connection['causality'] = 'source'
|
|
||||||
|
# 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", []):
|
for block in graph.get("blocks", []):
|
||||||
if isinstance(block, dict):
|
if isinstance(block, dict):
|
||||||
@@ -104,31 +137,167 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
|||||||
visit(graph)
|
visit(graph)
|
||||||
return id_list
|
return id_list
|
||||||
|
|
||||||
def get_ports_from_bond(bond: dict[str, Any], id_list: dict[str, Any]) -> tuple[dict[str,Any], dict[str,Any]]:
|
def is_port_fully_assigned(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> bool:
|
||||||
source = bond.get('source', None)
|
block_id = block.get('id')
|
||||||
target = bond.get('target', None)
|
port_id = port.get('id')
|
||||||
if source is None or target is None:
|
for connection in graph.get('connections', []):
|
||||||
raise ValueError("Source and Target of a power bond cannot be None")
|
if connection.get('type') != 'power':
|
||||||
|
continue
|
||||||
source_component = id_list.get(source.get('block'), None)
|
source = connection.get('source', {})
|
||||||
target_component = id_list.get(target.get('block'), None)
|
target = connection.get('target', {})
|
||||||
if source_component is None or target_component is None:
|
if ((source.get('block') == block_id and source.get('port') == port_id)
|
||||||
raise ValueError("Source or Target blocks not found")
|
or (target.get('block') == block_id and target.get('port') == port_id)):
|
||||||
|
if connection.get('causality', 'none') == 'none':
|
||||||
def _find_port(ports: list[dict[str, Any]], port: str) -> dict[str, Any] | None:
|
return False
|
||||||
for pi in ports:
|
return True
|
||||||
if pi.get('id', '') == port:
|
|
||||||
return pi
|
|
||||||
return None
|
|
||||||
|
|
||||||
source_port = _find_port(source_component.get('interface', {}).get('ports', []), source.get('port'))
|
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:
|
||||||
target_port = _find_port(target_component.get('interface', {}).get('ports', []), target.get('port'))
|
# find all connection on this specific port
|
||||||
if source_port is None or target_port is None:
|
attached_connections = []
|
||||||
raise ValueError("Source or Target port not found")
|
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':
|
# find first unassigned connection on this port
|
||||||
raise ValueError("Source port is not a power port")
|
target_connection = None
|
||||||
if target_port.get('type') != 'power':
|
for con in attached_connections:
|
||||||
raise ValueError("Target port is not a power port")
|
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 uuid import uuid4
|
||||||
|
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
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]:
|
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")
|
raise ValueError(f"Power port {port.name!r} has an unknown domain")
|
||||||
if port.causality not in POWER_CAUSALITIES:
|
if port.causality not in POWER_CAUSALITIES:
|
||||||
raise ValueError(f"Power port {port.name!r} has invalid causality")
|
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():
|
if not port.value_type.strip():
|
||||||
raise ValueError(f"Port {port.name!r} has no value type")
|
raise ValueError(f"Port {port.name!r} has no value type")
|
||||||
if port.rows < 1 or port.columns < 1:
|
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])
|
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 flow out",
|
||||||
"fixed effort out",
|
"fixed effort out",
|
||||||
"preferred flow out",
|
"preferred flow out",
|
||||||
@@ -29,3 +29,21 @@ POWER_CAUSALITIES = (
|
|||||||
"likes effort out",
|
"likes effort out",
|
||||||
"indifferent",
|
"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:
|
) -> None:
|
||||||
"""Compose and retain the active graph's Modelica representation."""
|
"""Compose and retain the active graph's Modelica representation."""
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
self.model_path = None
|
self.model_path = None
|
||||||
self.compose_source(graph)
|
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.simulation import Simulation
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
from bedit.core.serializer import DocumentSerializer
|
from bedit.core.serializer import DocumentSerializer
|
||||||
|
from bedit.gui.preferences import application_settings
|
||||||
|
|
||||||
|
|
||||||
class DocumentController(QObject):
|
class DocumentController(QObject):
|
||||||
@@ -479,7 +480,9 @@ class DocumentController(QObject):
|
|||||||
error_callback,
|
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."""
|
"""Infer causality and copy the derived values into the live model."""
|
||||||
|
|
||||||
inferred = infer_causality(component.to_dict())
|
inferred = infer_causality(component.to_dict())
|
||||||
@@ -505,7 +508,8 @@ class DocumentController(QObject):
|
|||||||
if changed:
|
if changed:
|
||||||
if self.document is not None:
|
if self.document is not None:
|
||||||
self.document.validate()
|
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:
|
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
||||||
item = (
|
item = (
|
||||||
@@ -1175,6 +1179,10 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
||||||
self._graph_for(owner_id).connections[connection.id] = connection
|
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:
|
if owner_id == self.active_component_id:
|
||||||
self.connectionAdded.emit(connection.id)
|
self.connectionAdded.emit(connection.id)
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
@@ -1211,10 +1219,20 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||||
self._graph_for(owner_id).connections.pop(connection_id, 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:
|
if owner_id == self.active_component_id:
|
||||||
self.connectionRemoved.emit(connection_id)
|
self.connectionRemoved.emit(connection_id)
|
||||||
self.documentReset.emit()
|
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:
|
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
||||||
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
||||||
if owner_id == self.active_component_id:
|
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.model import Component, Port
|
||||||
from bedit.core.physical_types import QUANTITIES, UNITS
|
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.core.port_types import PortTypeRegistry
|
||||||
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
||||||
|
|
||||||
@@ -37,14 +37,16 @@ class PortOptionsDialog(QDialog):
|
|||||||
self.ui.orientationCombo.setItemData(2, "indifferent")
|
self.ui.orientationCombo.setItemData(2, "indifferent")
|
||||||
for domain in POWER_DOMAINS:
|
for domain in POWER_DOMAINS:
|
||||||
self.ui.domainCombo.addItem(domain.display_name, domain.id)
|
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.portList.currentRowChanged.connect(self._load_current)
|
||||||
self.ui.addPortButton.clicked.connect(self.add_port)
|
self.ui.addPortButton.clicked.connect(self.add_port)
|
||||||
self.ui.removePortButton.clicked.connect(self.remove_port)
|
self.ui.removePortButton.clicked.connect(self.remove_port)
|
||||||
self.ui.nameEdit.textEdited.connect(self._store_current)
|
self.ui.nameEdit.textEdited.connect(self._store_current)
|
||||||
self.ui.typeCombo.currentIndexChanged.connect(self._port_type_changed)
|
self.ui.typeCombo.currentIndexChanged.connect(self._port_type_changed)
|
||||||
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
|
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.valueTypeCombo.currentTextChanged.connect(self._store_current)
|
||||||
self.ui.quantityCombo.currentTextChanged.connect(self._store_current)
|
self.ui.quantityCombo.currentTextChanged.connect(self._store_current)
|
||||||
self.ui.unitCombo.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.columnsSpin.setValue(port.columns)
|
||||||
self.ui.descriptionEdit.setPlainText(port.description)
|
self.ui.descriptionEdit.setPlainText(port.description)
|
||||||
self.ui.domainCombo.setCurrentIndex(self.ui.domainCombo.findData(port.domain))
|
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.causalityCombo.setCurrentText(port.causality)
|
||||||
self.ui.powerDescriptionEdit.setPlainText(port.description)
|
self.ui.powerDescriptionEdit.setPlainText(port.description)
|
||||||
self._show_type_options(port.type)
|
self._show_type_options(port.type)
|
||||||
@@ -120,9 +127,36 @@ class PortOptionsDialog(QDialog):
|
|||||||
indifferent_item.setEnabled(port_type == "power")
|
indifferent_item.setEnabled(port_type == "power")
|
||||||
if port_type != "power" and self.ui.orientationCombo.currentData() == "indifferent":
|
if port_type != "power" and self.ui.orientationCombo.currentData() == "indifferent":
|
||||||
self.ui.orientationCombo.setCurrentIndex(0)
|
self.ui.orientationCombo.setCurrentIndex(0)
|
||||||
|
self._refresh_causality_options(port_type=port_type)
|
||||||
self._show_type_options(port_type)
|
self._show_type_options(port_type)
|
||||||
self._store_current()
|
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:
|
def _show_type_options(self, port_type: str) -> None:
|
||||||
self.ui.orientationCombo.model().item(2).setEnabled(port_type == "power")
|
self.ui.orientationCombo.model().item(2).setEnabled(port_type == "power")
|
||||||
page = {
|
page = {
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ class SettingsDialog(QDialog):
|
|||||||
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
|
||||||
self.ui.graphSnapSpinBox.setValue(self.graph_snap_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.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.ui.openModelicaPathEdit.setText(self.openmodelica_path(self.settings))
|
||||||
self._load_syntax_styles()
|
self._load_syntax_styles()
|
||||||
self._update_remove_button()
|
self._update_remove_button()
|
||||||
@@ -132,6 +135,15 @@ class SettingsDialog(QDialog):
|
|||||||
settings = settings if settings is not None else application_settings()
|
settings = settings if settings is not None else application_settings()
|
||||||
return settings.value("grid/iconSize", 8, type=int)
|
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
|
@staticmethod
|
||||||
def openmodelica_path(settings: QSettings | None = None) -> str:
|
def openmodelica_path(settings: QSettings | None = None) -> str:
|
||||||
settings = settings if settings is not None else application_settings()
|
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/graphSize", self.ui.graphGridSpinBox.value())
|
||||||
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
|
||||||
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
|
||||||
|
self.settings.setValue(
|
||||||
|
"bondGraph/inferCausalityOnConnectionChange",
|
||||||
|
self.ui.automaticCausalityCheckBox.isChecked(),
|
||||||
|
)
|
||||||
self.settings.setValue(
|
self.settings.setValue(
|
||||||
"simulation/openModelicaPath",
|
"simulation/openModelicaPath",
|
||||||
self.ui.openModelicaPathEdit.text().strip(),
|
self.ui.openModelicaPathEdit.text().strip(),
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
|
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
|
||||||
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
|
QDialog, QDialogButtonBox, QFormLayout, QGroupBox,
|
||||||
QHeaderView, QLabel, QLineEdit, QListWidget,
|
QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
||||||
QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem,
|
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||||
QSpinBox, QTabWidget, QTableWidget, QTableWidgetItem,
|
QSpacerItem, QSpinBox, QTabWidget, QTableWidget,
|
||||||
QVBoxLayout, QWidget)
|
QTableWidgetItem, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_SettingsDialog(object):
|
class Ui_SettingsDialog(object):
|
||||||
def setupUi(self, SettingsDialog):
|
def setupUi(self, SettingsDialog):
|
||||||
@@ -104,6 +104,34 @@ class Ui_SettingsDialog(object):
|
|||||||
self.generalLayout.addItem(self.generalSpacer)
|
self.generalLayout.addItem(self.generalSpacer)
|
||||||
|
|
||||||
self.settingsTabs.addTab(self.generalTab, "")
|
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 = QWidget()
|
||||||
self.simulationTab.setObjectName(u"simulationTab")
|
self.simulationTab.setObjectName(u"simulationTab")
|
||||||
self.simulationTabLayout = QVBoxLayout(self.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.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None))
|
||||||
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", 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.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.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None))
|
||||||
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", 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))
|
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.
|
# This is the intentional code-level styling point for every port/connection type.
|
||||||
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
|
||||||
"signal": 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.name_label: NameLabelItem | None = None
|
||||||
self.sync_name_label(connection)
|
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:
|
def sync_name_label(self, connection: Connection) -> None:
|
||||||
visible = bool(connection.properties.get("showName", False))
|
visible = bool(connection.properties.get("showName", False))
|
||||||
if not visible:
|
if not visible:
|
||||||
|
|||||||
@@ -86,6 +86,55 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="bondGraphTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Bond graph</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="bondGraphLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="causalityGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Causality</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="causalityLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="automaticCausalityCheckBox">
|
||||||
|
<property name="text">
|
||||||
|
<string>Infer causality when connections are added or removed</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="automaticCausalityHintLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>When disabled, displayed causalities are updated only when compiling, exporting, or running the model.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="bondGraphSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
<widget class="QWidget" name="simulationTab">
|
<widget class="QWidget" name="simulationTab">
|
||||||
<attribute name="title"><string>Simulation</string></attribute>
|
<attribute name="title"><string>Simulation</string></attribute>
|
||||||
<layout class="QVBoxLayout" name="simulationTabLayout">
|
<layout class="QVBoxLayout" name="simulationTabLayout">
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"orientation": "indifferent",
|
"orientation": "indifferent",
|
||||||
"domain": "power",
|
"domain": "power",
|
||||||
"causality": "indifferent"
|
"causality": "single flow in"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -451,11 +451,431 @@
|
|||||||
"initialEquations": ""
|
"initialEquations": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "140ae14b-9dbb-4756-aa25-29101ef2a99e",
|
||||||
|
"name": "0",
|
||||||
|
"position": {
|
||||||
|
"x": -96.0,
|
||||||
|
"y": -32.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "port-3e53bf80",
|
||||||
|
"name": "p",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 64.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"multipleConnections": true,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "indifferent",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "single effort in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parameters": [],
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "0",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 32.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"showName": false
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": "effort = p[1].e;\nsum(p[i].f for i in 1:$p_N$) = 0;\nfor i in 2:$p_N$ loop\n p[i].e = p[i-1].e;\nend for;",
|
||||||
|
"declarations": "Real effort;",
|
||||||
|
"initialEquations": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6a6fbadd-28ec-4d10-b854-876752a6b287",
|
||||||
|
"name": "C0",
|
||||||
|
"position": {
|
||||||
|
"x": -96.0,
|
||||||
|
"y": 96.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "port-2e1d884f",
|
||||||
|
"name": "p",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 64.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"multipleConnections": false,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "input",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "preferred effort out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port-ca7716d5",
|
||||||
|
"name": "state",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 88.0,
|
||||||
|
"y": 40.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal",
|
||||||
|
"multipleConnections": false,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "output",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "indifferent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "parameter-d056bc27",
|
||||||
|
"name": "c",
|
||||||
|
"type": "real",
|
||||||
|
"value": "1",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "C",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 32.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"showName": false
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": "der(state) = p.f;\np.e = state/c;",
|
||||||
|
"declarations": "",
|
||||||
|
"initialEquations": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9c1aa152-f9fb-459a-9911-e90dd5a9beb3",
|
||||||
|
"name": "R0",
|
||||||
|
"position": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": -32.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "port-2e1d884f",
|
||||||
|
"name": "p",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 64.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"multipleConnections": false,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "input",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "indifferent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "parameter-d056bc27",
|
||||||
|
"name": "r",
|
||||||
|
"type": "real",
|
||||||
|
"value": "1",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "R",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 32.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"showName": false
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": "p.e = r*p.f;",
|
||||||
|
"declarations": "",
|
||||||
|
"initialEquations": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f37555f8-8a8b-455a-bd6d-a71343a23002",
|
||||||
|
"name": "I",
|
||||||
|
"position": {
|
||||||
|
"x": -224.0,
|
||||||
|
"y": -32.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "port-2e1d884f",
|
||||||
|
"name": "p",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 64.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"multipleConnections": false,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "input",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "preferred flow out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port-ca7716d5",
|
||||||
|
"name": "state",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 88.0,
|
||||||
|
"y": 40.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal",
|
||||||
|
"multipleConnections": false,
|
||||||
|
"valueType": "real",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"orientation": "output",
|
||||||
|
"domain": "power",
|
||||||
|
"causality": "indifferent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "parameter-d056bc27",
|
||||||
|
"name": "i",
|
||||||
|
"type": "real",
|
||||||
|
"value": "1",
|
||||||
|
"quantity": "",
|
||||||
|
"unit": "",
|
||||||
|
"dimensions": {
|
||||||
|
"rows": 1,
|
||||||
|
"columns": 1
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Text",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "I",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 32.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"showName": false
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "text",
|
||||||
|
"source": {
|
||||||
|
"equations": "der(state) = p.e;\np.f = state/i;",
|
||||||
|
"declarations": "",
|
||||||
|
"initialEquations": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{
|
{
|
||||||
"id": "4b09ba21-baa0-4db7-9431-4c919478bce4",
|
"id": "55c04603-2ae5-44d2-b3f3-493a73e88d73",
|
||||||
"source": {
|
"source": {
|
||||||
"block": "063a6341-cb18-4bfe-a1e4-00957a88a874",
|
"block": "063a6341-cb18-4bfe-a1e4-00957a88a874",
|
||||||
"port": "port-2e1d884f"
|
"port": "port-2e1d884f"
|
||||||
@@ -469,16 +889,16 @@
|
|||||||
"waypoints": []
|
"waypoints": []
|
||||||
},
|
},
|
||||||
"type": "power",
|
"type": "power",
|
||||||
"causality": "none"
|
"causality": "target"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a73a99a1-54e2-461d-a9a7-2d6d11d500f4",
|
"id": "5e7f801a-eb4a-46d2-9279-b4e81e1e011b",
|
||||||
"source": {
|
"source": {
|
||||||
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
|
"block": "140ae14b-9dbb-4756-aa25-29101ef2a99e",
|
||||||
"port": "port-3e53bf80"
|
"port": "port-3e53bf80"
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"block": "ea95e8ee-7f2d-4e14-a65d-d56bec5b4eb2",
|
"block": "9c1aa152-f9fb-459a-9911-e90dd5a9beb3",
|
||||||
"port": "port-2e1d884f"
|
"port": "port-2e1d884f"
|
||||||
},
|
},
|
||||||
"name": "",
|
"name": "",
|
||||||
@@ -486,10 +906,27 @@
|
|||||||
"waypoints": []
|
"waypoints": []
|
||||||
},
|
},
|
||||||
"type": "power",
|
"type": "power",
|
||||||
"causality": "none"
|
"causality": "target"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dd5fb1e2-b125-4b5f-a792-dac89dd89ad3",
|
"id": "ae95fbfc-c771-47f2-8f9e-1c823daaf6b8",
|
||||||
|
"source": {
|
||||||
|
"block": "140ae14b-9dbb-4756-aa25-29101ef2a99e",
|
||||||
|
"port": "port-3e53bf80"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "6a6fbadd-28ec-4d10-b854-876752a6b287",
|
||||||
|
"port": "port-2e1d884f"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"waypoints": []
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "45886c5b-8dec-4212-b13d-1f2996710a6b",
|
||||||
"source": {
|
"source": {
|
||||||
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
|
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
|
||||||
"port": "port-3e53bf80"
|
"port": "port-3e53bf80"
|
||||||
@@ -503,7 +940,58 @@
|
|||||||
"waypoints": []
|
"waypoints": []
|
||||||
},
|
},
|
||||||
"type": "power",
|
"type": "power",
|
||||||
"causality": "none"
|
"causality": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "476a20b4-ff02-4186-ac09-53f91ed06fcd",
|
||||||
|
"source": {
|
||||||
|
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
|
||||||
|
"port": "port-3e53bf80"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "ea95e8ee-7f2d-4e14-a65d-d56bec5b4eb2",
|
||||||
|
"port": "port-2e1d884f"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"waypoints": []
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "48b58a43-4d83-43ec-b502-1ff038445696",
|
||||||
|
"source": {
|
||||||
|
"block": "140ae14b-9dbb-4756-aa25-29101ef2a99e",
|
||||||
|
"port": "port-3e53bf80"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "f37555f8-8a8b-455a-bd6d-a71343a23002",
|
||||||
|
"port": "port-2e1d884f"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"waypoints": []
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d6940de3-5b4c-42be-a247-639b3803d85e",
|
||||||
|
"source": {
|
||||||
|
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
|
||||||
|
"port": "port-3e53bf80"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "140ae14b-9dbb-4756-aa25-29101ef2a99e",
|
||||||
|
"port": "port-3e53bf80"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"waypoints": []
|
||||||
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "source"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"annotations": [],
|
"annotations": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user