diff --git a/src/bedit_core/modelica/__init__.py b/src/bedit_core/modelica/__init__.py new file mode 100644 index 0000000..e2c0ce1 --- /dev/null +++ b/src/bedit_core/modelica/__init__.py @@ -0,0 +1,6 @@ +from .compose import CompositionResult, compose_modelica_model + +__all__ = [ + "CompositionResult", + "compose_modelica_model" +] \ No newline at end of file diff --git a/src/bedit_core/modelica/compose.py b/src/bedit_core/modelica/compose.py new file mode 100644 index 0000000..6d6707a --- /dev/null +++ b/src/bedit_core/modelica/compose.py @@ -0,0 +1,260 @@ +from __future__ import annotations +from dataclasses import dataclass +import re +from bedit_core.models import ( + Component, + BondPort, + GraphImplementation, + ValueType, + SignalDirection, + Port, + SignalPort, + PortID, + Parameter, + ParameterID, + EquationImplementation, + BondConnection, + SignalConnection, + ComponentID +) + +_BEVALUE_PATTERN = re.compile(r"\$([A-Za-z0-9_-]+)\$") + +class CompositionError(ValueError): + pass + +@dataclass +class CompositionResult: + modelica: str = "" + model_name: str = "" + +def compose_modelica_model(root: Component) -> CompositionResult: + composer = Composer(root) + return composer.compose() + +class Composer(): + def __init__(self, root: Component)-> None: + self.model : str = "" + self.root : Component = root + self.indent: int = 0 + self.lines: list[str] = [] + + def compose(self) -> CompositionResult: + self.indent = 0 + self.lines = [] + + self.emit_model(self.root) + + # Expand macros + macros = self.derive_macros(self.root, {}) + for i in range(len(self.lines)): + self.lines[i] = self.expand_bevalues(self.lines[i], macros) + + self.model = '\n'.join(self.lines) + return CompositionResult(self.model, self.model_name_for(self.root)) + + def emit_model(self, component: Component) -> None: + indentation = '\t'*self.indent + body_indentation = '\t'*(self.indent+1) + model_name = self.model_name_for(component) + macros: dict[str, str] = {} + + self.lines.append(f"{indentation}model {model_name}") + + # Add BondPort definition + if self.indent == 0: + self.lines.extend([ + f"{body_indentation}connector BondPort", + f"{body_indentation}\tReal e;", + f"{body_indentation}\tflow Real f;", + f"{body_indentation}end BondPort;", + ]) + + # Emit subcomponents + if isinstance(component.implementation, GraphImplementation): + for c_id, c in component.implementation.graph.components.items(): + self.indent += 1 + self.emit_model(c) + self.indent -= 1 + + # Child models have already expanded their own name-based macros. + lines_start = len(self.lines) + + # Interface + for port_id, port in component.interface.ports.items(): + self.lines.append(f"{body_indentation}{self.port_declaration(port)};") + if port.multiplicity: + macros[f"{port.name}_N"] = f"${port_id}_N$" + macros[f"{port.name}_S"] = f"${port_id}_S$" + + # Paramters + for param_id, param in component.parameters.items(): + self.lines.append(f"{body_indentation}{self.param_declaration(param)};") + + # declaration block if equation mode + if isinstance(component.implementation, EquationImplementation): + for line in component.implementation.declarations: + self.lines.append(f"{body_indentation}{line}") + + # Component instantiations in graph mode + if isinstance(component.implementation, GraphImplementation): + for c_id, c in component.implementation.graph.components.items(): + self.lines.append(f"{body_indentation}{self.model_name_for(c)} {c.name};") + + # Initial equations if equation mode + if isinstance(component.implementation, EquationImplementation): + if len(component.implementation.initial_equations): + self.lines.append(f"{indentation}initial equation") + for line in component.implementation.initial_equations: + self.lines.append(f"{body_indentation}{line}") + + self.lines.append(f"{indentation}equation") + + # equations block if equation mode + if isinstance(component.implementation, EquationImplementation): + for line in component.implementation.equations: + self.lines.append(f"{body_indentation}{line}") + + # Connections if graph mode + elif isinstance(component.implementation, GraphImplementation): + port_indices: dict[PortID, int] = {} + for c_id, c in component.implementation.graph.connections.items(): + source = component.implementation.graph.components[self.get_component_from_port(component, c.source)] + target = component.implementation.graph.components[self.get_component_from_port(component, c.target)] + source_port_name = self.port_reference(source.interface.ports[c.source], c.source, port_indices) + target_port_name = self.port_reference(target.interface.ports[c.target], c.target, port_indices) + if isinstance(c, BondConnection): + self.lines.append(f"{body_indentation}connect({source.name}.{source_port_name}, {target.name}.{target_port_name});") + elif isinstance(c, SignalConnection): + self.lines.append(f"{body_indentation}{target.name}.{target_port_name} = {source.name}.{source_port_name};") + else: + raise CompositionError(f"Unknown connection type {type(c).__name__}") + + else: + raise CompositionError(f"Unknown implementation type {type(component).__name__}") + + # Fill in macros + for i in range(lines_start, len(self.lines)): + try: + self.lines[i] = self.expand_bevalues(self.lines[i], macros) + except CompositionError: + # Accept unresolved macros + pass + + self.lines.append(f"{indentation}end {model_name};") + + @staticmethod + def model_name_for(component: Component) -> str: + # TODO make sure model name is safe + return f"m_{component.name}" + + @staticmethod + def modelica_type_from_ValueType(type: ValueType) -> str: + if type == ValueType.REAL: + return "Real" + elif type == ValueType.INT: + return "Integer" + elif type == ValueType.BOOL: + return "Boolean" + raise CompositionError(f"Unknown value type {type}") + + @classmethod + def port_declaration(cls, port: Port) -> str: + direction = "output" if port.direction == SignalDirection.OUTPUT else "input" + name = port.name + dimensions = cls.port_dimensions(port) + if isinstance(port, BondPort): + return f"BondPort {name}{dimensions}" + elif isinstance(port, SignalPort): + port_type = cls.modelica_type_from_ValueType(port.value_type) + #TODO units + return f"{direction} {port_type} {name}{dimensions}" + raise CompositionError(f"Unknown port type {type(port).__name__}") + + @classmethod + def fixed_dimensions(cls, p: Port | Parameter) -> str: + dimensions = p.matrix_size + rows = max(1, p.matrix_size[0]) + columns = max(1, p.matrix_size[1]) + if rows == columns == 1: + return "" + if columns == 1: + return f"[{rows}]" + return f"[{rows},{columns}]" + + @classmethod + def port_dimensions(cls, port: Port) -> str: + fixed = cls.fixed_dimensions(port) + if not port.multiplicity: + return fixed + entries = [f"${port.name}_N$"] + if fixed: + entries.extend(fixed[1:-1].split(",")) + return f"[{','.join(entries)}]" + + @staticmethod + def expand_bevalues(text, values: dict[str, str]) -> str: + """Replace BEdit ``$name$`` macros and reject unresolved composer values.""" + def replace(match: re.Match[str]) -> str: + name = match.group(1) + if name not in values: + raise CompositionError(f"Unknown BEdit value ${name}$") + return values[name] + return _BEVALUE_PATTERN.sub(replace, text) + + @classmethod + def param_declaration(cls, param: Parameter) -> str: + dimensions = cls.fixed_dimensions(param) + type = cls.modelica_type_from_ValueType(param.value_type) + # TODO units + return f"parameter {type} {param.name}{dimensions} = {param.value}" + + def get_component_from_port(self, component: Component, port_id: PortID) -> ComponentID: + if isinstance(component.implementation, GraphImplementation): + for c_id, c in component.implementation.graph.components.items(): + for p_id, p in c.interface.ports.items(): + if p_id == port_id: + return c_id + + raise CompositionError(f"Could not find component of port {port_id}") + + @staticmethod + def port_reference(port: Port, port_id: PortID, indices: dict[PortID, int],) -> str: + """Return a port name with its one-based multiplicity index.""" + if not port.multiplicity: + return port.name + index = indices.get(port_id, 0) + 1 + indices[port_id] = index + return f"{port.name}[{index}]" + + def derive_macros(self, component: Component, macros: dict[str, str]) -> dict[str, str]: + for port_id, port in component.interface.ports.items(): + if port.multiplicity: + macros.setdefault(f"{port_id}_N", "0") + macros.setdefault(f"{port_id}_S", "{}") + + if isinstance(component.implementation, GraphImplementation): + for c_id, c in component.implementation.graph.connections.items(): + source = component.implementation.graph.components[self.get_component_from_port(component, c.source)] + target = component.implementation.graph.components[self.get_component_from_port(component, c.target)] + if source.interface.ports[c.source].multiplicity: + self.record_multiplicity(macros, c.source, -1) + if target.interface.ports[c.target].multiplicity: + self.record_multiplicity(macros, c.target, 1) + + for c_id, c in component.implementation.graph.components.items(): + macros = self.derive_macros(c, macros) + + return macros + + @staticmethod + def record_multiplicity(macros: dict[str, str], port_id: PortID, sign: int) -> None: + """Record a connection count and orientation for a multiplicity port.""" + count_key = f"{port_id}_N" + signs_key = f"{port_id}_S" + macros[count_key] = str(int(macros.get(count_key, "0")) + 1) + signs = macros.get(signs_key, "{}") + entries = [] if signs == "{}" else signs[1:-1].split(", ") + entries.append(str(float(sign))) + macros[signs_key] = "{" + ", ".join(entries) + "}" + diff --git a/src/bedit_core/models.py b/src/bedit_core/models.py index 0d197e1..66b8b25 100644 --- a/src/bedit_core/models.py +++ b/src/bedit_core/models.py @@ -49,6 +49,11 @@ class PortCausality(Enum): SINGLE_EFFORT_IN = "single_effort_in" SINGLE_FLOW_IN = "single_flow_in" +class ValueType(Enum): + REAL = "real" + BOOL = "bool" + INT = "int" + @dataclass class Document: format_version: int @@ -78,7 +83,7 @@ class Port: @dataclass class SignalPort(Port): - value_type: str = "Real" + value_type: ValueType = ValueType.REAL quantity: str | None = None unit: str | None = None @@ -91,7 +96,8 @@ class BondPort(Port): class Parameter: name: str value: Any = 1.0 - value_type: str = "Real" + value_type: ValueType = ValueType.REAL + matrix_size: Annotated[list[int], 2] = field(default_factory=lambda: [1, 1]) quantity: str | None = None unit: str | None = None description: str | None = None diff --git a/src/bedit_core/serialization/schema.py b/src/bedit_core/serialization/schema.py index a3edca7..3191e36 100644 --- a/src/bedit_core/serialization/schema.py +++ b/src/bedit_core/serialization/schema.py @@ -28,6 +28,7 @@ from bedit_core.models import ( SignalConnection, SignalDirection, SignalPort, + ValueType ) class SerializationError(ValueError): @@ -118,7 +119,7 @@ def _port_to_data(port: Port) -> dict[str, Any]: return { "port_type": "signal", **common, - "value_type": port.value_type, + "value_type": _data_to_valuetype(port.value_type), "quantity": port.quantity, "unit": port.unit, } @@ -131,7 +132,6 @@ def _port_to_data(port: Port) -> dict[str, Any]: } raise TypeError(f"unsupported port class: {type(port).__name__}") - def _port_from_data(value: Any, where: str) -> Port: """Construct the port subclass selected by ``port_type``.""" obj = _mapping(value, where) @@ -146,7 +146,7 @@ def _port_from_data(value: Any, where: str) -> Port: if port_type == "signal": return SignalPort( **common, - value_type=_string(obj.get("value_type", "Real"), f"{where}.value_type"), + value_type=_valuetype_to_data(_string(obj.get("value_type", "Real"), f"{where}.value_type")), quantity=_optional_string(obj.get("quantity"), f"{where}.quantity"), unit=_optional_string(obj.get("unit"), f"{where}.unit"), ) @@ -162,13 +162,32 @@ def _port_from_data(value: Any, where: str) -> Port: ) raise SerializationError(f"{where}.port_type: unsupported value {port_type!r}") +def _data_to_valuetype(type: ValueType) -> str: + if type == ValueType.REAL: + return "real" + elif type == ValueType.INT: + return "int" + elif type == ValueType.BOOL: + return "bool" + raise SerializationError(f"Unknown value type {type}") + +def _valuetype_to_data(type: str) -> ValueType: + type = type.lower() + if type == "real": + return ValueType.REAL + elif type == "int": + return ValueType.INT + elif type == "bool": + return ValueType.BOOL + raise SerializationError(f"Unknown value type {type}") def _parameter_to_data(parameter: Parameter) -> dict[str, Any]: """Convert a parameter to raw values.""" return { "name": parameter.name, "value": _plain_value(parameter.value), - "value_type": parameter.value_type, + "value_type": _data_to_valuetype(parameter.value_type), + "matrix_size": list(parameter.matrix_size), "quantity": parameter.quantity, "unit": parameter.unit, "description": parameter.description, @@ -181,7 +200,8 @@ def _parameter_from_data(value: Any, where: str) -> Parameter: return Parameter( name=_string(_required(obj, "name", where), f"{where}.name"), value=obj.get("value", 1.0), - value_type=_string(obj.get("value_type", "Real"), f"{where}.value_type"), + value_type=_valuetype_to_data(_string(obj.get("value_type", "Real"), f"{where}.value_type")), + matrix_size=_matrix_size(obj.get("matrix_size", [1, 1]), f"{where}.matrix_size"), quantity=_optional_string(obj.get("quantity"), f"{where}.quantity"), unit=_optional_string(obj.get("unit"), f"{where}.unit"), description=_optional_string(obj.get("description"), f"{where}.description"), diff --git a/tests/manual_test.py b/tests/manual_test.py index df982fa..d72b855 100644 --- a/tests/manual_test.py +++ b/tests/manual_test.py @@ -1,11 +1,14 @@ import sys from bedit_core.serialization import load, save from bedit_core.bondgraph import causality_inference +from bedit_core.modelica import compose_modelica_model def main(path: str): doc = load(path) for id, root in doc.root.items(): doc.root[id] = causality_inference(doc.root[id]) + composition = compose_modelica_model(doc.root[id]) + print(composition.modelica) save(doc, path) if __name__ == "__main__": diff --git a/untitled.bedit.json b/untitled.bedit.json index e8d9957..87baf04 100644 --- a/untitled.bedit.json +++ b/untitled.bedit.json @@ -5,7 +5,7 @@ "name": "Current Document", "root": { "50e6ef97-f686-4400-bc01-e5a352e8cc22": { - "name": "New Graph Block 1", + "name": "test", "interface": { "ports": {} }, @@ -51,7 +51,11 @@ "parameter-d056bc27": { "name": "c", "value": "1", - "value_type": "Real", + "value_type": "real", + "matrix_size": [ + 1, + 1 + ], "quantity": null, "unit": null, "description": null @@ -128,7 +132,11 @@ "parameter-d056bc27": { "name": "r", "value": "1", - "value_type": "Real", + "value_type": "real", + "matrix_size": [ + 1, + 1 + ], "quantity": null, "unit": null, "description": null @@ -166,7 +174,11 @@ "parameter-d056bc27": { "name": "e", "value": "1", - "value_type": "Real", + "value_type": "real", + "matrix_size": [ + 1, + 1 + ], "quantity": null, "unit": null, "description": null @@ -242,7 +254,11 @@ "parameter-d056bc27": { "name": "r", "value": "10", - "value_type": "Real", + "value_type": "real", + "matrix_size": [ + 1, + 1 + ], "quantity": null, "unit": null, "description": null @@ -294,7 +310,11 @@ "parameter-d056bc27": { "name": "i", "value": "1", - "value_type": "Real", + "value_type": "real", + "matrix_size": [ + 1, + 1 + ], "quantity": null, "unit": null, "description": null