Start with causality inference
This commit is contained in:
@@ -40,6 +40,7 @@ from bedit.core.model import (
|
||||
Port,
|
||||
clone_component,
|
||||
)
|
||||
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
|
||||
@@ -307,6 +308,7 @@ class DocumentController(QObject):
|
||||
properties={
|
||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||
},
|
||||
type=source_port.type,
|
||||
)
|
||||
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||
return connection.id
|
||||
@@ -324,6 +326,8 @@ class DocumentController(QObject):
|
||||
if original is None:
|
||||
raise ValueError("The connection no longer exists")
|
||||
port_type = self.connection_port_type(original)
|
||||
if port_type == "power":
|
||||
raise ValueError("Power bond connections cannot contain junctions")
|
||||
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
||||
first_properties = deepcopy(original.properties)
|
||||
first_properties["waypoints"] = [
|
||||
@@ -338,6 +342,8 @@ class DocumentController(QObject):
|
||||
Endpoint(junction=junction.id),
|
||||
original.name,
|
||||
first_properties,
|
||||
original.type,
|
||||
original.causality,
|
||||
)
|
||||
second = Connection(
|
||||
str(uuid4()),
|
||||
@@ -345,6 +351,8 @@ class DocumentController(QObject):
|
||||
original.target,
|
||||
"",
|
||||
second_properties,
|
||||
original.type,
|
||||
original.causality,
|
||||
)
|
||||
self.undo_stack.push(
|
||||
SplitConnectionCommand(
|
||||
@@ -442,12 +450,14 @@ class DocumentController(QObject):
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before composing")
|
||||
self._infer_active_graph_causality(component)
|
||||
self.simulation.compose(component.to_dict())
|
||||
|
||||
def compose_active_graph_source(self) -> tuple[str, str]:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before exporting a model")
|
||||
self._infer_active_graph_causality(component)
|
||||
return self.simulation.compose_source(component.to_dict())
|
||||
|
||||
def run_simulation(
|
||||
@@ -460,6 +470,7 @@ class DocumentController(QObject):
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before running a simulation")
|
||||
self._infer_active_graph_causality(component)
|
||||
self.simulation.run_simulation(
|
||||
component.to_dict(),
|
||||
progress_callback,
|
||||
@@ -468,6 +479,34 @@ class DocumentController(QObject):
|
||||
error_callback,
|
||||
)
|
||||
|
||||
def _infer_active_graph_causality(self, component: Component) -> None:
|
||||
"""Infer causality and copy the derived values into the live model."""
|
||||
|
||||
inferred = infer_causality(component.to_dict())
|
||||
causalities: dict[str, str] = {}
|
||||
|
||||
def collect(serialized_component: dict) -> None:
|
||||
graph = serialized_component.get("implementation", {}).get("graph", {})
|
||||
for connection in graph.get("connections", []):
|
||||
causalities[str(connection["id"])] = str(
|
||||
connection.get("causality", "none")
|
||||
)
|
||||
for block in graph.get("blocks", []):
|
||||
collect(block)
|
||||
|
||||
collect(inferred)
|
||||
changed = False
|
||||
for nested_component in self._component_subtree(component):
|
||||
for connection in nested_component.graph.connections.values():
|
||||
causality = causalities.get(connection.id, "none")
|
||||
if connection.causality != causality:
|
||||
connection.causality = causality
|
||||
changed = True
|
||||
if changed:
|
||||
if self.document is not None:
|
||||
self.document.validate()
|
||||
self.documentReset.emit()
|
||||
|
||||
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
||||
item = (
|
||||
self.active_graph.connections
|
||||
@@ -577,25 +616,26 @@ class DocumentController(QObject):
|
||||
allows_multiple_connections=role == "source",
|
||||
)
|
||||
if endpoint.interface is not None:
|
||||
ports = owner.inputs if role == "source" else owner.outputs
|
||||
orientations = (
|
||||
{"input", "indifferent"}
|
||||
if role == "source"
|
||||
else {"output", "indifferent"}
|
||||
)
|
||||
ports = [
|
||||
*ports,
|
||||
*(
|
||||
port
|
||||
for port in owner.inputs
|
||||
if port.orientation == "indifferent" and port not in ports
|
||||
),
|
||||
port for port in owner.ports if port.orientation in orientations
|
||||
]
|
||||
else:
|
||||
component = owner.graph.blocks.get(endpoint.block or "")
|
||||
if component is None:
|
||||
return None
|
||||
ports = component.outputs if role == "source" else component.inputs
|
||||
if role == "source":
|
||||
ports = [
|
||||
*ports,
|
||||
*(port for port in component.inputs if port.orientation == "indifferent"),
|
||||
]
|
||||
orientations = (
|
||||
{"output", "indifferent"}
|
||||
if role == "source"
|
||||
else {"input", "indifferent"}
|
||||
)
|
||||
ports = [
|
||||
port for port in component.ports if port.orientation in orientations
|
||||
]
|
||||
return next(
|
||||
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
||||
)
|
||||
@@ -619,12 +659,13 @@ class DocumentController(QObject):
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before adding an interface")
|
||||
ports = component.inputs if direction == "input" else component.outputs
|
||||
ports = [port for port in component.ports if port.orientation == direction]
|
||||
port = Port(
|
||||
id=f"{direction}-{uuid4().hex[:8]}",
|
||||
name=f"{direction.title()} {len(ports) + 1}",
|
||||
x=position.x(),
|
||||
y=position.y(),
|
||||
orientation=direction,
|
||||
)
|
||||
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
||||
return port.id
|
||||
@@ -639,7 +680,7 @@ class DocumentController(QObject):
|
||||
owner = self.active_component
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||
if port is not None and port.name != name:
|
||||
self.undo_stack.push(
|
||||
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
||||
@@ -670,8 +711,7 @@ class DocumentController(QObject):
|
||||
|
||||
def replace_active_text_definition(
|
||||
self,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
ports: list[Port],
|
||||
declarations: str,
|
||||
initial_equations: str,
|
||||
equations: str,
|
||||
@@ -680,14 +720,17 @@ class DocumentController(QObject):
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "text":
|
||||
raise ValueError("Only text-defined components can be edited here")
|
||||
input_ids = [port.id for port in inputs]
|
||||
output_ids = [port.id for port in outputs]
|
||||
source_ids = output_ids + [
|
||||
port.id for port in inputs if port.orientation == "indifferent"
|
||||
]
|
||||
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
||||
raise ValueError("Input and output IDs must be unique")
|
||||
if any(not port.name.strip() for port in (*inputs, *outputs)):
|
||||
port_ids = [port.id for port in ports]
|
||||
input_ids = {
|
||||
port.id for port in ports if port.orientation in {"input", "indifferent"}
|
||||
}
|
||||
output_ids = {
|
||||
port.id for port in ports if port.orientation in {"output", "indifferent"}
|
||||
}
|
||||
source_ids = output_ids
|
||||
if len(set(port_ids)) != len(port_ids):
|
||||
raise ValueError("Port IDs must be unique")
|
||||
if any(not port.name.strip() for port in ports):
|
||||
raise ValueError("Every port must have a name")
|
||||
parameter_ids = [parameter.id for parameter in parameters]
|
||||
if len(set(parameter_ids)) != len(parameter_ids):
|
||||
@@ -713,14 +756,12 @@ class DocumentController(QObject):
|
||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||
)
|
||||
old = {
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"ports": [port.to_dict() for port in component.ports],
|
||||
"source": deepcopy(component.source),
|
||||
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
||||
}
|
||||
new = {
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"ports": [port.to_dict() for port in ports],
|
||||
"source": {
|
||||
"equations": equations,
|
||||
"declarations": declarations,
|
||||
@@ -731,8 +772,7 @@ class DocumentController(QObject):
|
||||
if old != new:
|
||||
candidate = deepcopy(self.document)
|
||||
candidate_component = candidate.find_component(component.id)
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate_component.ports = deepcopy(ports)
|
||||
candidate_component.source = deepcopy(new["source"])
|
||||
candidate_component.parameters = deepcopy(parameters)
|
||||
candidate.validate()
|
||||
@@ -743,8 +783,7 @@ class DocumentController(QObject):
|
||||
component_id: str,
|
||||
name: str,
|
||||
icon: Icon,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
ports: list[Port],
|
||||
show_subtree: bool,
|
||||
show_name: bool,
|
||||
) -> None:
|
||||
@@ -759,8 +798,7 @@ class DocumentController(QObject):
|
||||
old = {
|
||||
"name": component.name,
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"ports": [port.to_dict() for port in component.ports],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
"properties": deepcopy(component.properties),
|
||||
}
|
||||
@@ -772,8 +810,7 @@ class DocumentController(QObject):
|
||||
new = {
|
||||
"name": name,
|
||||
"icon": icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"ports": [port.to_dict() for port in ports],
|
||||
"show_subtree": show_subtree,
|
||||
"properties": properties,
|
||||
}
|
||||
@@ -782,8 +819,7 @@ class DocumentController(QObject):
|
||||
candidate_component = candidate.find_component(component_id)
|
||||
candidate_component.name = name
|
||||
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate_component.ports = deepcopy(ports)
|
||||
candidate_component.properties = deepcopy(properties)
|
||||
candidate.validate()
|
||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||
@@ -853,19 +889,19 @@ class DocumentController(QObject):
|
||||
)
|
||||
)
|
||||
|
||||
def edit_component_ports(
|
||||
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
||||
) -> None:
|
||||
def edit_component_ports(self, component_id: str, ports: list[Port]) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
input_ids = {port.id for port in inputs}
|
||||
output_ids = {port.id for port in outputs}
|
||||
source_ids = output_ids | {
|
||||
port.id for port in inputs if port.orientation == "indifferent"
|
||||
input_ids = {
|
||||
port.id for port in ports if port.orientation in {"input", "indifferent"}
|
||||
}
|
||||
output_ids = {
|
||||
port.id for port in ports if port.orientation in {"output", "indifferent"}
|
||||
}
|
||||
source_ids = output_ids
|
||||
parent = self.document.find_parent(component_id)
|
||||
if parent is not None:
|
||||
for connection in parent.graph.connections.values():
|
||||
@@ -886,21 +922,18 @@ class DocumentController(QObject):
|
||||
raise ValueError("An interface output cannot be removed while connected")
|
||||
candidate = deepcopy(self.document)
|
||||
candidate_component = candidate.find_component(component_id)
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate_component.ports = deepcopy(ports)
|
||||
candidate.validate()
|
||||
old = {
|
||||
"name": component.name,
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"ports": [port.to_dict() for port in component.ports],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
"properties": deepcopy(component.properties),
|
||||
}
|
||||
new = {
|
||||
**old,
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"ports": [port.to_dict() for port in ports],
|
||||
}
|
||||
if old != new:
|
||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||
@@ -930,8 +963,7 @@ class DocumentController(QObject):
|
||||
self,
|
||||
block_ids: set[str],
|
||||
connection_ids: set[str],
|
||||
input_ids: set[str],
|
||||
output_ids: set[str],
|
||||
port_ids: set[str],
|
||||
) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
@@ -942,8 +974,8 @@ class DocumentController(QObject):
|
||||
if (
|
||||
connection.source.block in block_ids
|
||||
or connection.target.block in block_ids
|
||||
or connection.source.interface in input_ids
|
||||
or connection.target.interface in output_ids
|
||||
or connection.source.interface in port_ids
|
||||
or connection.target.interface in port_ids
|
||||
):
|
||||
all_connection_ids.add(connection.id)
|
||||
blocks = {
|
||||
@@ -954,9 +986,8 @@ class DocumentController(QObject):
|
||||
for connection_id in all_connection_ids
|
||||
if connection_id in graph.connections
|
||||
}
|
||||
inputs = [port for port in component.inputs if port.id in input_ids]
|
||||
outputs = [port for port in component.outputs if port.id in output_ids]
|
||||
if not (blocks or connections or inputs or outputs):
|
||||
ports = [port for port in component.ports if port.id in port_ids]
|
||||
if not (blocks or connections or ports):
|
||||
return
|
||||
self.undo_stack.push(
|
||||
DeleteSelectionCommand(
|
||||
@@ -964,8 +995,7 @@ class DocumentController(QObject):
|
||||
component.id,
|
||||
blocks,
|
||||
connections,
|
||||
inputs,
|
||||
outputs,
|
||||
ports,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1003,6 +1033,8 @@ class DocumentController(QObject):
|
||||
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||
name=source.name,
|
||||
properties=properties,
|
||||
type=source.type,
|
||||
causality=source.causality,
|
||||
)
|
||||
connections[connection.id] = connection
|
||||
if blocks:
|
||||
@@ -1063,6 +1095,8 @@ class DocumentController(QObject):
|
||||
),
|
||||
name=source.name,
|
||||
properties=properties,
|
||||
type=source.type,
|
||||
causality=source.causality,
|
||||
)
|
||||
connections[connection.id] = connection
|
||||
|
||||
@@ -1278,9 +1312,9 @@ class DocumentController(QObject):
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
ports = owner.inputs if direction == "input" else owner.outputs
|
||||
if all(existing.id != port.id for existing in ports):
|
||||
ports.append(port)
|
||||
port.orientation = direction
|
||||
if all(existing.id != port.id for existing in owner.ports):
|
||||
owner.ports.append(port)
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
@@ -1290,8 +1324,7 @@ class DocumentController(QObject):
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
ports = owner.inputs if direction == "input" else owner.outputs
|
||||
ports[:] = [port for port in ports if port.id != port_id]
|
||||
owner.ports[:] = [port for port in owner.ports if port.id != port_id]
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
@@ -1301,7 +1334,7 @@ class DocumentController(QObject):
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||
if port is not None:
|
||||
port.x, port.y = position.x(), position.y()
|
||||
self.interfaceChanged.emit()
|
||||
@@ -1311,7 +1344,7 @@ class DocumentController(QObject):
|
||||
owner = self.document.find_component(owner_id) if self.document else None
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||
if port is not None:
|
||||
port.name = name
|
||||
self.interfaceChanged.emit()
|
||||
@@ -1342,8 +1375,7 @@ class DocumentController(QObject):
|
||||
return
|
||||
component.name = values["name"]
|
||||
component.icon = Icon.from_dict(values["icon"])
|
||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
||||
component.ports = [Port.from_dict(port) for port in values["ports"]]
|
||||
component.show_subtree_in_library = values["show_subtree"]
|
||||
component.properties = deepcopy(values["properties"])
|
||||
self.documentReset.emit()
|
||||
@@ -1374,8 +1406,7 @@ class DocumentController(QObject):
|
||||
owner_id: str | None,
|
||||
block_ids: set[str],
|
||||
connection_ids: set[str],
|
||||
input_ids: set[str],
|
||||
output_ids: set[str],
|
||||
port_ids: set[str],
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
@@ -1398,8 +1429,7 @@ class DocumentController(QObject):
|
||||
owner.graph.blocks.pop(block_id, None)
|
||||
for connection_id in connection_ids:
|
||||
owner.graph.connections.pop(connection_id, None)
|
||||
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
||||
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
||||
owner.ports[:] = [port for port in owner.ports if port.id not in port_ids]
|
||||
if active_was_deleted:
|
||||
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
||||
self.activeGraphChanged.emit()
|
||||
@@ -1410,8 +1440,7 @@ class DocumentController(QObject):
|
||||
owner_id: str | None,
|
||||
blocks: dict[str, Component],
|
||||
connections: dict[str, Connection],
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
ports: list[Port],
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
@@ -1423,10 +1452,8 @@ class DocumentController(QObject):
|
||||
return
|
||||
owner.graph.blocks.update(blocks)
|
||||
owner.graph.connections.update(connections)
|
||||
existing_inputs = {port.id for port in owner.inputs}
|
||||
existing_outputs = {port.id for port in owner.outputs}
|
||||
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
|
||||
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
|
||||
existing_ports = {port.id for port in owner.ports}
|
||||
owner.ports.extend(port for port in ports if port.id not in existing_ports)
|
||||
self.documentReset.emit()
|
||||
|
||||
@staticmethod
|
||||
@@ -1441,8 +1468,7 @@ class DocumentController(QObject):
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
||||
component.ports = [Port.from_dict(item) for item in values["ports"]]
|
||||
component.source = deepcopy(values["source"])
|
||||
component.parameters = [
|
||||
Parameter.from_dict(item) for item in values.get("parameters", [])
|
||||
|
||||
Reference in New Issue
Block a user