From edefb23edcda05b6abfbb17bf409ff55ba4f3625 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Wed, 22 Jul 2026 11:38:07 +0200 Subject: [PATCH] More types and units --- BEdit/AGENTS.md | 13 + BEdit/m_Test.mo | 21 + BEdit/src/bedit/core/model.py | 66 +- BEdit/src/bedit/core/physical_types.py | 47 ++ BEdit/src/bedit/core/simulation/__init__.py | 3 +- BEdit/src/bedit/core/simulation/composer.py | 45 +- .../src/bedit/core/simulation/openmodelica.py | 147 +++- BEdit/src/bedit/core/simulation/results.py | 15 + BEdit/src/bedit/core/simulation/service.py | 15 +- .../bedit/gui/dialogs/parameter_options.py | 43 +- BEdit/src/bedit/gui/dialogs/port_options.py | 47 +- .../src/bedit/gui/editors/text_definition.py | 46 +- .../generated/ui_parameter_options_dialog.py | 93 ++- .../gui/generated/ui_port_options_dialog.py | 114 ++- .../generated/ui_text_definition_editor.py | 82 +- BEdit/src/bedit/gui/simulation_window.py | 57 +- BEdit/ui/parameter_options_dialog.ui | 219 +++++- BEdit/ui/port_options_dialog.ui | 279 ++++++- BEdit/ui/text_definition_editor.ui | 4 +- BEdit/untitled.bedit.json | 724 ++---------------- m_Test.mo | 35 +- 21 files changed, 1355 insertions(+), 760 deletions(-) create mode 100644 BEdit/m_Test.mo create mode 100644 BEdit/src/bedit/core/physical_types.py diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 1f173ee..4cc2184 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -60,6 +60,10 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant indexes inputs and outputs separately for connection semantics. - Port types are registered in `core/port_types.py`. Only compatible types may be connected. `signal` is currently the only type. +- Port connector type and signal value type are separate. Signal ports and + parameters carry editable value type, quantity, unit, row/column dimensions, + and description metadata. Editable quantity/unit suggestions live in + `core/physical_types.py`; unlisted values remain valid. - Port removal or reorientation must be rejected when it would invalidate an existing connection. - Connections reference port IDs, never port names. @@ -168,6 +172,15 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant `simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must not read `QSettings`. An empty path uses OMPython/PATH discovery, while an explicit `.../bin/omc` path is converted to the OpenModelica home directory. +- Builds enable OpenModelica's `--unitChecking`. Completed simulation results + read per-column units from OMC's generated `_init.xml`; BEdit must not + infer derivative units itself. This metadata is serialized with `.ber`/JSON + results and displayed by the simulation signal tree and plots. +- Result loading restores variables marked `alias` or `negatedAlias` in OMC's + initialization XML. Values come from OMC's result representative or a fixed + parameter/constant start value; BEdit does not infer aliases from graph edges. +- Compile requests call OMC `checkModel` before `buildModel` and log OMC's check + summary verbatim through the application-wide logger. ## Qt Designer and generated files diff --git a/BEdit/m_Test.mo b/BEdit/m_Test.mo new file mode 100644 index 0000000..340f2fb --- /dev/null +++ b/BEdit/m_Test.mo @@ -0,0 +1,21 @@ +model m_Test + model m_Constant0 + output Real y(quantity="Length", unit="m"); + parameter Real v = 2; + equation + y = v; + end m_Constant0; + model m_Gain0 + input Real u; + output Real y; + parameter Real k(quantity="Current", unit="A") = 2.5; + equation + y = k*u; + end m_Gain0; + m_Constant0 Constant0; + m_Gain0 Gain0; +equation + Gain0.u = Constant0.y; +annotation( + experiment(StartTime = 0, StopTime = 1, Tolerance = 1e-06, Interval = 0.002)); +end m_Test; \ No newline at end of file diff --git a/BEdit/src/bedit/core/model.py b/BEdit/src/bedit/core/model.py index c2a68e6..f6c220b 100644 --- a/BEdit/src/bedit/core/model.py +++ b/BEdit/src/bedit/core/model.py @@ -8,6 +8,19 @@ from uuid import uuid4 from bedit.core.port_types import PortTypeRegistry +def _dimensions(data: Any, subject: str) -> tuple[int, int]: + if not isinstance(data, dict): + raise ValueError(f"{subject} dimensions must be an object") + try: + rows = int(data.get("rows", 1)) + columns = int(data.get("columns", 1)) + except (TypeError, ValueError) as error: + raise ValueError(f"{subject} dimensions must be whole numbers") from error + if rows < 1 or columns < 1: + raise ValueError(f"{subject} dimensions must be at least 1") + return rows, columns + + @dataclass class Port: id: str @@ -17,6 +30,12 @@ class Port: properties: dict[str, Any] = field(default_factory=dict) type: str = "signal" allows_multiple_connections: bool = False + value_type: str = "real" + quantity: str = "" + unit: str = "" + rows: int = 1 + columns: int = 1 + description: str = "" def to_dict(self) -> dict[str, Any]: return { @@ -26,11 +45,17 @@ class Port: "properties": self.properties, "type": self.type, "multipleConnections": self.allows_multiple_connections, + "valueType": self.value_type, + "quantity": self.quantity, + "unit": self.unit, + "dimensions": {"rows": self.rows, "columns": self.columns}, + "description": self.description, } @classmethod def from_dict(cls, data: dict[str, Any]) -> "Port": position = data.get("position", {}) + rows, columns = _dimensions(data.get("dimensions", {}), "Port") return cls( id=str(data["id"]), name=str(data.get("name", data["id"])), @@ -39,6 +64,12 @@ class Port: properties=dict(data.get("properties", {})), type=str(data.get("type", "signal")), allows_multiple_connections=bool(data.get("multipleConnections", False)), + value_type=str(data.get("valueType", "real")), + quantity=str(data.get("quantity", "")), + unit=str(data.get("unit", "")), + rows=rows, + columns=columns, + description=str(data.get("description", "")), ) @@ -166,9 +197,23 @@ class Parameter: name: str type: str = "real" value: str = "0" + quantity: str = "" + unit: str = "" + rows: int = 1 + columns: int = 1 + description: str = "" - def to_dict(self) -> dict[str, str]: - return {"id": self.id, "name": self.name, "type": self.type, "value": self.value} + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "name": self.name, + "type": self.type, + "value": self.value, + "quantity": self.quantity, + "unit": self.unit, + "dimensions": {"rows": self.rows, "columns": self.columns}, + "description": self.description, + } @classmethod def from_dict(cls, data: dict[str, Any]) -> "Parameter": @@ -176,11 +221,17 @@ class Parameter: raise ValueError("Each component parameter must be an object") if "id" not in data: raise ValueError("Each component parameter must have an ID") + rows, columns = _dimensions(data.get("dimensions", {}), "Parameter") return cls( id=str(data["id"]), name=str(data.get("name", "")), type=str(data.get("type", "real")), value=str(data.get("value", "0")), + quantity=str(data.get("quantity", "")), + unit=str(data.get("unit", "")), + rows=rows, + columns=columns, + description=str(data.get("description", "")), ) @@ -453,6 +504,13 @@ class GraphDocument: raise ValueError(f"Component {component.name!r} has duplicate parameter names") if any(not name.strip() for name in parameter_names): raise ValueError(f"Component {component.name!r} has an unnamed parameter") + if any(not parameter.type.strip() for parameter in component.parameters): + raise ValueError(f"Component {component.name!r} has an untyped parameter") + if any( + parameter.rows < 1 or parameter.columns < 1 + for parameter in component.parameters + ): + raise ValueError(f"Component {component.name!r} has invalid parameter dimensions") if component.implementation_kind == "text" and component.graph.blocks: raise ValueError(f"Text component {component.name} cannot contain a graph") self._validate_graph(component) @@ -470,6 +528,10 @@ class GraphDocument: raise ValueError(f"Component {owner.name} contains duplicate port IDs") for port in (*owner.inputs, *owner.outputs): PortTypeRegistry.get(port.type) + 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: + raise ValueError(f"Port {port.name!r} has invalid dimensions") for junction in owner.graph.junctions.values(): PortTypeRegistry.get(junction.type) endpoint_counts: dict[tuple[str, str, str], int] = {} diff --git a/BEdit/src/bedit/core/physical_types.py b/BEdit/src/bedit/core/physical_types.py new file mode 100644 index 0000000..070d62f --- /dev/null +++ b/BEdit/src/bedit/core/physical_types.py @@ -0,0 +1,47 @@ +"""Editable suggestions for signal and parameter physical metadata. + +These values populate editable combo boxes; documents may use values not listed +here. They are suggestions rather than a validation registry. +""" + +VALUE_TYPES = ("real", "integer", "boolean", "string") + +QUANTITIES = ( + "", + "Angle", + "AngularVelocity", + "Current", + "Energy", + "Force", + "Frequency", + "Length", + "Mass", + "Power", + "Pressure", + "Temperature", + "Time", + "Torque", + "Velocity", + "Voltage", +) + +UNITS = ( + "", + "1", + "A", + "Hz", + "J", + "K", + "N", + "N.m", + "Pa", + "V", + "W", + "deg", + "kg", + "m", + "m/s", + "rad", + "rad/s", + "s", +) diff --git a/BEdit/src/bedit/core/simulation/__init__.py b/BEdit/src/bedit/core/simulation/__init__.py index 76866cf..14d4fce 100644 --- a/BEdit/src/bedit/core/simulation/__init__.py +++ b/BEdit/src/bedit/core/simulation/__init__.py @@ -1,5 +1,5 @@ from bedit.core.simulation.service import Simulation -from bedit.core.simulation.openmodelica import OpenModelicaInterface +from bedit.core.simulation.openmodelica import ModelBuildResult, OpenModelicaInterface from bedit.core.simulation.results import ( BerSimulationResultsSerializer, JsonSimulationResultsSerializer, @@ -12,6 +12,7 @@ from bedit.core.simulation.results import ( __all__ = [ "OpenModelicaInterface", + "ModelBuildResult", "Simulation", "SimulationResults", "SimulationExecutionResult", diff --git a/BEdit/src/bedit/core/simulation/composer.py b/BEdit/src/bedit/core/simulation/composer.py index 59cd668..3328be5 100644 --- a/BEdit/src/bedit/core/simulation/composer.py +++ b/BEdit/src/bedit/core/simulation/composer.py @@ -119,8 +119,11 @@ def emit_model( parameter_type = modelica_type(parameter.get("type", "real")) parameter_name = identifier(parameter["name"]) value = expand_bevalues(str(parameter.get("value", "0")), macros) + dimensions = _fixed_dimensions(parameter) + attributes = _quantity_unit_attributes(parameter) lines.append( - f"{body_indent}parameter {parameter_type} {parameter_name} = {value};" + f"{body_indent}parameter {parameter_type} {parameter_name}" + f"{dimensions}{attributes} = {value};" ) if implementation_kind == "text": @@ -213,14 +216,48 @@ def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]: def _port_declaration( port: dict[str, Any], direction: str, indent: int, macros: dict[str, str] ) -> str: - port_type = modelica_type(port.get("type", "signal")) + port_type = modelica_type(port.get("valueType", "real")) indentation = "\t" * indent port_name = identifier(port["name"]) - dimension = f"[${port_name}_N$]" if port.get("multipleConnections", False) else "" - declaration = f"{indentation}{direction} {port_type} {port_name}{dimension};" + dimensions = _port_dimensions(port, port_name) + attributes = _quantity_unit_attributes(port) + declaration = ( + f"{indentation}{direction} {port_type} {port_name}{dimensions}{attributes};" + ) return expand_bevalues(declaration, macros) +def _fixed_dimensions(item: dict[str, Any]) -> str: + dimensions = item.get("dimensions", {}) + rows = max(1, int(dimensions.get("rows", 1))) + columns = max(1, int(dimensions.get("columns", 1))) + if rows == columns == 1: + return "" + if columns == 1: + return f"[{rows}]" + return f"[{rows},{columns}]" + + +def _port_dimensions(port: dict[str, Any], port_name: str) -> str: + fixed = _fixed_dimensions(port) + if not port.get("multipleConnections", False): + return fixed + entries = [f"${port_name}_N$"] + if fixed: + entries.extend(fixed[1:-1].split(",")) + return f"[{','.join(entries)}]" + + +def _quantity_unit_attributes(item: dict[str, Any]) -> str: + attributes = [] + for key in ("quantity", "unit"): + value = str(item.get(key, "")).strip() + if value: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + attributes.append(f'{key}="{escaped}"') + return f"({', '.join(attributes)})" if attributes else "" + + def _endpoint_expression( endpoint: dict[str, Any], owner: dict[str, Any], diff --git a/BEdit/src/bedit/core/simulation/openmodelica.py b/BEdit/src/bedit/core/simulation/openmodelica.py index c2a1899..e8c0226 100644 --- a/BEdit/src/bedit/core/simulation/openmodelica.py +++ b/BEdit/src/bedit/core/simulation/openmodelica.py @@ -48,6 +48,14 @@ class SimulationMessage: text: str +@dataclass(frozen=True) +class ModelBuildResult: + """OMC's model check summary and subsequent build response.""" + + check_summary: str + build_result: Any + + class OpenModelicaInterface: """Asynchronous, persistent interface to one OpenModelica session. @@ -92,10 +100,21 @@ class OpenModelicaInterface: """Load and build one composed model as an ordered worker operation.""" def operation(omc, _temp_dir: Path): + unit_checking = omc.sendExpression( + 'setCommandLineOptions("--unitChecking")' + ) + if unit_checking is not True: + raise RuntimeError("OpenModelica could not enable unit checking") loaded = omc.sendExpression(f"loadString({json.dumps(model)})") if loaded is not True: raise RuntimeError("OpenModelica could not load the composed model") - return omc.sendExpression(f"buildModel({model_name})") + check_summary = omc.sendExpression(f"checkModel({model_name})") + if not isinstance(check_summary, str) or not check_summary.strip(): + raise RuntimeError( + "OpenModelica did not return a model-check summary" + ) + build_result = omc.sendExpression(f"buildModel({model_name})") + return ModelBuildResult(check_summary, build_result) self._submit("build model", operation, callback, error_callback) @@ -334,16 +353,140 @@ def _run_model_with_tcp( f"OpenModelica did not create the expected result file {result_path.name!r}" ) data = load_openmodelica_csv(result_path) + init_path = temp_dir / f"{Path(executable).stem}_init.xml" + csv_column_count = len(data) + data = _add_openmodelica_alias_signals(init_path, data) + signal_metadata = _load_openmodelica_signal_metadata(init_path, data) log.info( - "Loaded %d result columns from %s", len(data), result_path.name + "Loaded %d result columns, restored %d OMC aliases, and found %d " + "unit definitions from %s", + csv_column_count, + len(data) - csv_column_count, + len(signal_metadata), + result_path.name, ) return SimulationExecutionResult( return_code=int(return_code), result_file=str(result_path), data=data, + signal_metadata=signal_metadata, ) +def _add_openmodelica_alias_signals( + path: Path, data: dict[str, list[float]] +) -> dict[str, list[float]]: + """Restore result aliases exactly as described by OMC's init XML.""" + + try: + root = ET.parse(path).getroot() + except (OSError, ET.ParseError) as error: + raise RuntimeError( + f"Could not read OpenModelica aliases from {path.name}: {error}" + ) from error + + variables: dict[str, dict[str, str]] = {} + for scalar in root.findall(".//ScalarVariable"): + name = scalar.get("name", "") + if not name: + continue + value = next(iter(scalar), None) + variables[name] = { + "alias": scalar.get("alias", "noAlias"), + "target": scalar.get("aliasVariable", ""), + "variability": scalar.get("variability", ""), + "start": value.get("start", "") if value is not None else "", + } + + restored = {name: list(values) for name, values in data.items()} + sample_count = len(next(iter(data.values()), [])) + + def resolve(name: str, visited: set[str] | None = None) -> list[float] | None: + if name in restored: + return restored[name] + variable = variables.get(name) + if variable is None: + return None + visited = set() if visited is None else visited + if name in visited: + return None + visited.add(name) + alias_kind = variable["alias"] + target = variable["target"] + if alias_kind in {"alias", "negatedAlias"} and target: + values = resolve(target, visited) + if values is None: + return None + return [-value for value in values] if alias_kind == "negatedAlias" else list(values) + if variable["variability"] not in {"parameter", "constant"}: + return None + try: + value = float(variable["start"]) + except ValueError: + return None + return [value] * sample_count + + for name, variable in variables.items(): + if variable["alias"] not in {"alias", "negatedAlias"} or name in restored: + continue + values = resolve(name) + if values is not None: + restored[name] = values + return restored + + +def _load_openmodelica_signal_metadata( + path: Path, signal_names: dict[str, list[float]] +) -> dict[str, dict[str, str]]: + """Read units reported by OMC in its generated initialization XML.""" + + try: + root = ET.parse(path).getroot() + except (OSError, ET.ParseError) as error: + raise RuntimeError( + f"Could not read OpenModelica unit metadata from {path.name}: {error}" + ) from error + + variables: dict[str, tuple[dict[str, str], str]] = {} + for scalar in root.findall(".//ScalarVariable"): + name = scalar.get("name", "") + if not name: + continue + value = next(iter(scalar), None) + metadata: dict[str, str] = {} + if value is not None: + for key in ("unit", "displayUnit", "quantity"): + attribute = value.get(key, "") + if attribute: + metadata[key] = attribute + description = scalar.get("description", "") + if description: + metadata["description"] = description + variables[name] = (metadata, scalar.get("aliasVariable", "")) + + def resolve(name: str, visited: set[str] | None = None) -> dict[str, str]: + metadata, alias = variables.get(name, ({}, "")) + if metadata.get("unit") or not alias: + return dict(metadata) + visited = set() if visited is None else visited + if name in visited: + return dict(metadata) + visited.add(name) + inherited = resolve(alias, visited) + return {**inherited, **metadata} + + metadata: dict[str, dict[str, str]] = {} + for name in signal_names: + if name not in variables: + continue + resolved = resolve(name) + if resolved: + metadata[name] = resolved + if "time" in signal_names: + metadata["time"] = {"unit": "s", "quantity": "Time"} + return metadata + + def _accept_simulation_connection( server: socket.socket, command_finished: Event ) -> socket.socket: diff --git a/BEdit/src/bedit/core/simulation/results.py b/BEdit/src/bedit/core/simulation/results.py index a962710..cbbced9 100644 --- a/BEdit/src/bedit/core/simulation/results.py +++ b/BEdit/src/bedit/core/simulation/results.py @@ -44,6 +44,7 @@ class SimulationResults: status: dict[str, Any] = field(default_factory=dict) messages: list[dict[str, str]] = field(default_factory=list) data: dict[str, list[float]] = field(default_factory=dict) + signal_metadata: dict[str, dict[str, str]] = field(default_factory=dict) graphs: list[SimulationGraph] = field( default_factory=lambda: [SimulationGraph()] ) @@ -57,6 +58,10 @@ class SimulationResults: "status": dict(self.status), "messages": [dict(message) for message in self.messages], "data": {name: list(values) for name, values in self.data.items()}, + "signalMetadata": { + name: dict(metadata) + for name, metadata in self.signal_metadata.items() + }, "graphs": [asdict(graph) for graph in self.graphs], "metadata": dict(self.metadata), } @@ -87,6 +92,15 @@ class SimulationResults: str(name): [float(value) for value in values] for name, values in dict(data.get("data", {})).items() }, + signal_metadata={ + str(name): { + str(key): str(value) + for key, value in dict(metadata).items() + } + for name, metadata in dict( + data.get("signalMetadata", {}) + ).items() + }, graphs=graphs, metadata=dict(data.get("metadata", {})), ) @@ -101,6 +115,7 @@ class SimulationExecutionResult: return_code: int result_file: str data: dict[str, list[float]] + signal_metadata: dict[str, dict[str, str]] = field(default_factory=dict) def load_openmodelica_csv(path: str | Path) -> dict[str, list[float]]: diff --git a/BEdit/src/bedit/core/simulation/service.py b/BEdit/src/bedit/core/simulation/service.py index 83182d3..32b2e1d 100644 --- a/BEdit/src/bedit/core/simulation/service.py +++ b/BEdit/src/bedit/core/simulation/service.py @@ -5,6 +5,7 @@ from bedit.core.application_log import get_logger from bedit.core.simulation.composer import compose_graph from bedit.core.simulation.openmodelica import ( ErrorCallback, + ModelBuildResult, OpenModelicaInterface, ResultCallback, SimulationMessage, @@ -50,9 +51,19 @@ class Simulation: self.compose_source(graph) def _model_compiled(result): - log.info("Compiling OK: %s", result) + if not isinstance(result, ModelBuildResult): + failure = RuntimeError( + f"OpenModelica returned an invalid build response: {result!r}" + ) + if error_callback is not None: + error_callback(failure) + else: + log.error("%s", failure) + return + log.info("%s", result.check_summary.strip()) + log.info("Compiling OK: %s", result.build_result) try: - self.model_path = str(result[0]) + self.model_path = str(result.build_result[0]) except (IndexError, TypeError) as error: failure = RuntimeError( f"OpenModelica returned an invalid build result: {result!r}" diff --git a/BEdit/src/bedit/gui/dialogs/parameter_options.py b/BEdit/src/bedit/gui/dialogs/parameter_options.py index 531ac3e..8923d55 100644 --- a/BEdit/src/bedit/gui/dialogs/parameter_options.py +++ b/BEdit/src/bedit/gui/dialogs/parameter_options.py @@ -5,6 +5,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox from bedit.core.model import Component, Parameter +from bedit.core.physical_types import QUANTITIES, UNITS from bedit.gui.generated.ui_parameter_options_dialog import Ui_ParameterOptionsDialog @@ -22,19 +23,31 @@ class ParameterOptionsDialog(QDialog): self.parameters = deepcopy(component.parameters) self.read_only = read_only self._loading = False + self.ui.quantityCombo.addItems(QUANTITIES) + self.ui.unitCombo.addItems(UNITS) self.ui.parameterList.currentRowChanged.connect(self._load_current) self.ui.addParameterButton.clicked.connect(self.add_parameter) self.ui.removeParameterButton.clicked.connect(self.remove_parameter) self.ui.nameEdit.textEdited.connect(self._store_current) - self.ui.typeEdit.textEdited.connect(self._store_current) + self.ui.typeCombo.currentTextChanged.connect(self._store_current) self.ui.valueEdit.textEdited.connect(self._store_current) + self.ui.quantityCombo.currentTextChanged.connect(self._store_current) + self.ui.unitCombo.currentTextChanged.connect(self._store_current) + self.ui.rowsSpin.valueChanged.connect(self._store_current) + self.ui.columnsSpin.valueChanged.connect(self._store_current) + self.ui.descriptionEdit.textChanged.connect(self._store_current) self.ui.parameterSplitter.setSizes([250, 370]) if read_only: self.ui.addParameterButton.setEnabled(False) self.ui.removeParameterButton.setEnabled(False) self.ui.nameEdit.setReadOnly(True) - self.ui.typeEdit.setReadOnly(True) + self.ui.typeCombo.setEnabled(False) self.ui.valueEdit.setReadOnly(True) + self.ui.quantityCombo.setEnabled(False) + self.ui.unitCombo.setEnabled(False) + self.ui.rowsSpin.setEnabled(False) + self.ui.columnsSpin.setEnabled(False) + self.ui.descriptionEdit.setReadOnly(True) self._rebuild_list(0 if self.parameters else -1) def _rebuild_list(self, row: int = -1) -> None: @@ -51,12 +64,20 @@ class ParameterOptionsDialog(QDialog): if 0 <= row < len(self.parameters): parameter = self.parameters[row] self.ui.nameEdit.setText(parameter.name) - self.ui.typeEdit.setText(parameter.type) + self.ui.typeCombo.setCurrentText(parameter.type) self.ui.valueEdit.setText(parameter.value) + self.ui.quantityCombo.setCurrentText(parameter.quantity) + self.ui.unitCombo.setCurrentText(parameter.unit) + self.ui.rowsSpin.setValue(parameter.rows) + self.ui.columnsSpin.setValue(parameter.columns) + self.ui.descriptionEdit.setPlainText(parameter.description) else: self.ui.nameEdit.clear() - self.ui.typeEdit.clear() + self.ui.typeCombo.setCurrentText("") self.ui.valueEdit.clear() + self.ui.quantityCombo.setCurrentText("") + self.ui.unitCombo.setCurrentText("") + self.ui.descriptionEdit.clear() self._loading = False self._update_enabled() @@ -64,8 +85,13 @@ class ParameterOptionsDialog(QDialog): enabled = self.ui.parameterList.currentRow() >= 0 self.ui.removeParameterButton.setEnabled(enabled and not self.read_only) self.ui.nameEdit.setEnabled(enabled) - self.ui.typeEdit.setEnabled(enabled) + self.ui.typeCombo.setEnabled(enabled and not self.read_only) self.ui.valueEdit.setEnabled(enabled) + self.ui.quantityCombo.setEnabled(enabled and not self.read_only) + self.ui.unitCombo.setEnabled(enabled and not self.read_only) + self.ui.rowsSpin.setEnabled(enabled and not self.read_only) + self.ui.columnsSpin.setEnabled(enabled and not self.read_only) + self.ui.descriptionEdit.setEnabled(enabled) def _store_current(self) -> None: row = self.ui.parameterList.currentRow() @@ -73,8 +99,13 @@ class ParameterOptionsDialog(QDialog): return parameter = self.parameters[row] parameter.name = self.ui.nameEdit.text() - parameter.type = self.ui.typeEdit.text() + parameter.type = self.ui.typeCombo.currentText().strip() or "real" parameter.value = self.ui.valueEdit.text() + parameter.quantity = self.ui.quantityCombo.currentText().strip() + parameter.unit = self.ui.unitCombo.currentText().strip() + parameter.rows = self.ui.rowsSpin.value() + parameter.columns = self.ui.columnsSpin.value() + parameter.description = self.ui.descriptionEdit.toPlainText() self.ui.parameterList.item(row).setText( f"{parameter.name} [{parameter.type}] = {parameter.value}" ) diff --git a/BEdit/src/bedit/gui/dialogs/port_options.py b/BEdit/src/bedit/gui/dialogs/port_options.py index 212f835..b256b61 100644 --- a/BEdit/src/bedit/gui/dialogs/port_options.py +++ b/BEdit/src/bedit/gui/dialogs/port_options.py @@ -5,6 +5,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessageBox from bedit.core.model import Component, Port +from bedit.core.physical_types import QUANTITIES, UNITS from bedit.core.port_types import PortTypeRegistry from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog @@ -20,6 +21,7 @@ class PortOptionsDialog(QDialog): self.ui = Ui_PortOptionsDialog() self.ui.setupUi(self) self.setWindowTitle(f"Port Options — {component.name}") + self.read_only = read_only self.ports: list[tuple[Port, str]] = [ *((deepcopy(port), "input") for port in component.inputs), *((deepcopy(port), "output") for port in component.outputs), @@ -28,15 +30,23 @@ class PortOptionsDialog(QDialog): self.ui.typeCombo.clear() for port_type in PortTypeRegistry.all(): self.ui.typeCombo.addItem(port_type.display_name, port_type.id) + self.ui.quantityCombo.addItems(QUANTITIES) + self.ui.unitCombo.addItems(UNITS) self.ui.orientationCombo.setItemData(0, "input") self.ui.orientationCombo.setItemData(1, "output") 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._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.valueTypeCombo.currentTextChanged.connect(self._store_current) + self.ui.quantityCombo.currentTextChanged.connect(self._store_current) + self.ui.unitCombo.currentTextChanged.connect(self._store_current) + self.ui.rowsSpin.valueChanged.connect(self._store_current) + self.ui.columnsSpin.valueChanged.connect(self._store_current) + self.ui.descriptionEdit.textChanged.connect(self._store_current) self.ui.portSplitter.setSizes([250, 370]) if read_only: self.ui.addPortButton.setEnabled(False) @@ -45,6 +55,7 @@ class PortOptionsDialog(QDialog): self.ui.typeCombo.setEnabled(False) self.ui.orientationCombo.setEnabled(False) self.ui.multipleConnectionsCheckBox.setEnabled(False) + self.ui.typeOptionsStack.setEnabled(False) self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close") self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide() self._rebuild_list(0 if self.ports else -1) @@ -77,18 +88,38 @@ class PortOptionsDialog(QDialog): self.ui.multipleConnectionsCheckBox.setChecked( port.allows_multiple_connections ) + self.ui.valueTypeCombo.setCurrentText(port.value_type) + self.ui.quantityCombo.setCurrentText(port.quantity) + self.ui.unitCombo.setCurrentText(port.unit) + self.ui.rowsSpin.setValue(port.rows) + self.ui.columnsSpin.setValue(port.columns) + self.ui.descriptionEdit.setPlainText(port.description) + self._show_type_options(port.type) else: self.ui.nameEdit.clear() + self.ui.descriptionEdit.clear() self._loading = False self._update_enabled() def _update_enabled(self) -> None: enabled = self.ui.portList.currentRow() >= 0 - self.ui.removePortButton.setEnabled(enabled) + self.ui.removePortButton.setEnabled(enabled and not self.read_only) self.ui.nameEdit.setEnabled(enabled) - self.ui.typeCombo.setEnabled(enabled) - self.ui.orientationCombo.setEnabled(enabled) - self.ui.multipleConnectionsCheckBox.setEnabled(enabled) + self.ui.typeCombo.setEnabled(enabled and not self.read_only) + self.ui.orientationCombo.setEnabled(enabled and not self.read_only) + self.ui.multipleConnectionsCheckBox.setEnabled(enabled and not self.read_only) + self.ui.typeOptionsStack.setEnabled(enabled and not self.read_only) + + def _port_type_changed(self) -> None: + self._show_type_options(self.ui.typeCombo.currentData()) + self._store_current() + + def _show_type_options(self, port_type: str) -> None: + self.ui.typeOptionsStack.setCurrentWidget( + self.ui.signalOptionsPage + if port_type == "signal" + else self.ui.unsupportedTypePage + ) def _store_current(self) -> None: row = self.ui.portList.currentRow() @@ -98,6 +129,12 @@ class PortOptionsDialog(QDialog): port.name = self.ui.nameEdit.text() port.type = self.ui.typeCombo.currentData() port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked() + port.value_type = self.ui.valueTypeCombo.currentText().strip() or "real" + port.quantity = self.ui.quantityCombo.currentText().strip() + port.unit = self.ui.unitCombo.currentText().strip() + port.rows = self.ui.rowsSpin.value() + port.columns = self.ui.columnsSpin.value() + port.description = self.ui.descriptionEdit.toPlainText() self.ports[row] = (port, self.ui.orientationCombo.currentData()) self.ui.portList.item(row).setText( f"{port.name} [{self.ports[row][1]}, {port.type}]" diff --git a/BEdit/src/bedit/gui/editors/text_definition.py b/BEdit/src/bedit/gui/editors/text_definition.py index 48094eb..b7dc884 100644 --- a/BEdit/src/bedit/gui/editors/text_definition.py +++ b/BEdit/src/bedit/gui/editors/text_definition.py @@ -26,10 +26,10 @@ class TextDefinitionEditor(QWidget): self._modified = False self._loading = False self.ui.portsTable.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeMode.Stretch + QHeaderView.ResizeMode.Interactive ) self.ui.parametersTable.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeMode.Stretch + QHeaderView.ResizeMode.Interactive ) self.ui.columnSplitter.setSizes([560, 340]) self.ui.sourceSplitter.setSizes([180, 180, 240]) @@ -78,6 +78,12 @@ class TextDefinitionEditor(QWidget): type=type_combo.currentData(), properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}), allows_multiple_connections=multiple_check.isChecked(), + value_type=self.ui.portsTable.item(row, 4).text().strip() or "real", + quantity=self.ui.portsTable.item(row, 5).text().strip(), + unit=self.ui.portsTable.item(row, 6).text().strip(), + rows=int(self.ui.portsTable.item(row, 7).text()), + columns=int(self.ui.portsTable.item(row, 8).text()), + description=self.ui.portsTable.item(row, 9).text(), ) target = inputs if orientation_combo.currentData() == "input" else outputs target.append(port) @@ -92,6 +98,11 @@ class TextDefinitionEditor(QWidget): name=table.item(row, 0).text().strip(), type=table.item(row, 1).text().strip(), value=table.item(row, 2).text(), + quantity=table.item(row, 3).text().strip(), + unit=table.item(row, 4).text().strip(), + rows=int(table.item(row, 5).text()), + columns=int(table.item(row, 6).text()), + description=table.item(row, 7).text(), ) for row in range(table.rowCount()) ] @@ -138,8 +149,17 @@ class TextDefinitionEditor(QWidget): self._mark_modified() def _refresh_editor_symbols(self) -> None: - inputs, outputs = self.ports - self._set_editor_symbols(inputs, outputs, self.parameters) + inputs: list[str] = [] + outputs: list[str] = [] + for row in range(self.ui.portsTable.rowCount()): + name_item = self.ui.portsTable.item(row, 0) + orientation = self.ui.portsTable.cellWidget(row, 2).currentData() + (inputs if orientation == "input" else outputs).append(name_item.text()) + parameters = [ + self.ui.parametersTable.item(row, 0).text() + for row in range(self.ui.parametersTable.rowCount()) + ] + self._apply_editor_symbols(inputs, outputs, parameters) def _set_editor_symbols( self, @@ -147,11 +167,16 @@ class TextDefinitionEditor(QWidget): outputs: list[Port], parameters: list[Parameter], ) -> None: - symbols = ( + self._apply_editor_symbols( [port.name for port in inputs], [port.name for port in outputs], [parameter.name for parameter in parameters], ) + + def _apply_editor_symbols( + self, inputs: list[str], outputs: list[str], parameters: list[str] + ) -> None: + symbols = (inputs, outputs, parameters) self.ui.declarationsEdit.set_symbols(*symbols) self.ui.initialEquationsEdit.set_symbols(*symbols) self.ui.equationsEdit.set_symbols(*symbols) @@ -183,6 +208,12 @@ class TextDefinitionEditor(QWidget): multiple.setChecked(port.allows_multiple_connections) multiple.toggled.connect(self._symbols_modified) table.setCellWidget(row, 3, multiple) + table.setItem(row, 4, QTableWidgetItem(port.value_type)) + table.setItem(row, 5, QTableWidgetItem(port.quantity)) + table.setItem(row, 6, QTableWidgetItem(port.unit)) + table.setItem(row, 7, QTableWidgetItem(str(port.rows))) + table.setItem(row, 8, QTableWidgetItem(str(port.columns))) + table.setItem(row, 9, QTableWidgetItem(port.description)) def _append_parameter(self, parameter: Parameter) -> None: table = self.ui.parametersTable @@ -193,6 +224,11 @@ class TextDefinitionEditor(QWidget): table.setItem(row, 0, name) table.setItem(row, 1, QTableWidgetItem(parameter.type)) table.setItem(row, 2, QTableWidgetItem(parameter.value)) + table.setItem(row, 3, QTableWidgetItem(parameter.quantity)) + table.setItem(row, 4, QTableWidgetItem(parameter.unit)) + table.setItem(row, 5, QTableWidgetItem(str(parameter.rows))) + table.setItem(row, 6, QTableWidgetItem(str(parameter.columns))) + table.setItem(row, 7, QTableWidgetItem(parameter.description)) def add_port(self) -> None: port = Port( diff --git a/BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py b/BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py index 1a3ad79..0c5436e 100644 --- a/BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py +++ b/BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py @@ -15,16 +15,17 @@ 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, QApplication, QDialog, QDialogButtonBox, - QFormLayout, QHBoxLayout, QLabel, QLineEdit, - QListWidget, QListWidgetItem, QPushButton, QSizePolicy, - QSplitter, QVBoxLayout, QWidget) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, + QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, + QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, + QPushButton, QSizePolicy, QSpinBox, QSplitter, + QVBoxLayout, QWidget) class Ui_ParameterOptionsDialog(object): def setupUi(self, ParameterOptionsDialog): if not ParameterOptionsDialog.objectName(): ParameterOptionsDialog.setObjectName(u"ParameterOptionsDialog") - ParameterOptionsDialog.resize(620, 380) + ParameterOptionsDialog.resize(720, 500) self.dialogLayout = QVBoxLayout(ParameterOptionsDialog) self.dialogLayout.setObjectName(u"dialogLayout") self.parameterSplitter = QSplitter(ParameterOptionsDialog) @@ -76,20 +77,82 @@ class Ui_ParameterOptionsDialog(object): self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel) - self.typeEdit = QLineEdit(self.parameterDetailsPanel) - self.typeEdit.setObjectName(u"typeEdit") + self.typeCombo = QComboBox(self.parameterDetailsPanel) + self.typeCombo.addItem("") + self.typeCombo.addItem("") + self.typeCombo.addItem("") + self.typeCombo.addItem("") + self.typeCombo.setObjectName(u"typeCombo") + self.typeCombo.setEditable(True) - self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.typeEdit) + self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.typeCombo) + + self.rowsLabel = QLabel(self.parameterDetailsPanel) + self.rowsLabel.setObjectName(u"rowsLabel") + + self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.rowsLabel) + + self.rowsSpin = QSpinBox(self.parameterDetailsPanel) + self.rowsSpin.setObjectName(u"rowsSpin") + self.rowsSpin.setMinimum(1) + self.rowsSpin.setMaximum(9999) + + self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.rowsSpin) + + self.columnsLabel = QLabel(self.parameterDetailsPanel) + self.columnsLabel.setObjectName(u"columnsLabel") + + self.parameterDetailsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.columnsLabel) + + self.columnsSpin = QSpinBox(self.parameterDetailsPanel) + self.columnsSpin.setObjectName(u"columnsSpin") + self.columnsSpin.setMinimum(1) + self.columnsSpin.setMaximum(9999) + + self.parameterDetailsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.columnsSpin) self.valueLabel = QLabel(self.parameterDetailsPanel) self.valueLabel.setObjectName(u"valueLabel") - self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.valueLabel) + self.parameterDetailsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.valueLabel) self.valueEdit = QLineEdit(self.parameterDetailsPanel) self.valueEdit.setObjectName(u"valueEdit") - self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.valueEdit) + self.parameterDetailsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.valueEdit) + + self.quantityLabel = QLabel(self.parameterDetailsPanel) + self.quantityLabel.setObjectName(u"quantityLabel") + + self.parameterDetailsForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.quantityLabel) + + self.quantityCombo = QComboBox(self.parameterDetailsPanel) + self.quantityCombo.setObjectName(u"quantityCombo") + self.quantityCombo.setEditable(True) + + self.parameterDetailsForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.quantityCombo) + + self.unitLabel = QLabel(self.parameterDetailsPanel) + self.unitLabel.setObjectName(u"unitLabel") + + self.parameterDetailsForm.setWidget(6, QFormLayout.ItemRole.LabelRole, self.unitLabel) + + self.unitCombo = QComboBox(self.parameterDetailsPanel) + self.unitCombo.setObjectName(u"unitCombo") + self.unitCombo.setEditable(True) + + self.parameterDetailsForm.setWidget(6, QFormLayout.ItemRole.FieldRole, self.unitCombo) + + self.descriptionLabel = QLabel(self.parameterDetailsPanel) + self.descriptionLabel.setObjectName(u"descriptionLabel") + + self.parameterDetailsForm.setWidget(7, QFormLayout.ItemRole.LabelRole, self.descriptionLabel) + + self.descriptionEdit = QPlainTextEdit(self.parameterDetailsPanel) + self.descriptionEdit.setObjectName(u"descriptionEdit") + self.descriptionEdit.setMaximumHeight(100) + + self.parameterDetailsForm.setWidget(7, QFormLayout.ItemRole.FieldRole, self.descriptionEdit) self.parameterSplitter.addWidget(self.parameterDetailsPanel) @@ -115,6 +178,16 @@ class Ui_ParameterOptionsDialog(object): self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None)) self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None)) self.typeLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Type:", None)) + self.typeCombo.setItemText(0, QCoreApplication.translate("ParameterOptionsDialog", u"real", None)) + self.typeCombo.setItemText(1, QCoreApplication.translate("ParameterOptionsDialog", u"integer", None)) + self.typeCombo.setItemText(2, QCoreApplication.translate("ParameterOptionsDialog", u"boolean", None)) + self.typeCombo.setItemText(3, QCoreApplication.translate("ParameterOptionsDialog", u"string", None)) + + self.rowsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Rows:", None)) + self.columnsLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Columns:", None)) self.valueLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Value:", None)) + self.quantityLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Quantity:", None)) + self.unitLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Unit:", None)) + self.descriptionLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Description:", None)) # retranslateUi diff --git a/BEdit/src/bedit/gui/generated/ui_port_options_dialog.py b/BEdit/src/bedit/gui/generated/ui_port_options_dialog.py index 4266200..eeb0f81 100644 --- a/BEdit/src/bedit/gui/generated/ui_port_options_dialog.py +++ b/BEdit/src/bedit/gui/generated/ui_port_options_dialog.py @@ -18,14 +18,14 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, - QPushButton, QSizePolicy, QSplitter, QVBoxLayout, - QWidget) + QPlainTextEdit, QPushButton, QSizePolicy, QSpinBox, + QSplitter, QStackedWidget, QVBoxLayout, QWidget) class Ui_PortOptionsDialog(object): def setupUi(self, PortOptionsDialog): if not PortOptionsDialog.objectName(): PortOptionsDialog.setObjectName(u"PortOptionsDialog") - PortOptionsDialog.resize(620, 380) + PortOptionsDialog.resize(760, 421) self.dialogLayout = QVBoxLayout(PortOptionsDialog) self.dialogLayout.setObjectName(u"dialogLayout") self.portSplitter = QSplitter(PortOptionsDialog) @@ -61,6 +61,7 @@ class Ui_PortOptionsDialog(object): self.portDetailsPanel.setObjectName(u"portDetailsPanel") self.portDetailsForm = QFormLayout(self.portDetailsPanel) self.portDetailsForm.setObjectName(u"portDetailsForm") + self.portDetailsForm.setVerticalSpacing(3) self.portDetailsForm.setContentsMargins(0, 0, 0, 0) self.nameLabel = QLabel(self.portDetailsPanel) self.nameLabel.setObjectName(u"nameLabel") @@ -104,7 +105,99 @@ class Ui_PortOptionsDialog(object): self.positionHintLabel.setObjectName(u"positionHintLabel") self.positionHintLabel.setWordWrap(True) - self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel) + self.portDetailsForm.setWidget(5, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel) + + self.typeOptionsStack = QStackedWidget(self.portDetailsPanel) + self.typeOptionsStack.setObjectName(u"typeOptionsStack") + self.signalOptionsPage = QWidget() + self.signalOptionsPage.setObjectName(u"signalOptionsPage") + self.signalOptionsForm = QFormLayout(self.signalOptionsPage) + self.signalOptionsForm.setObjectName(u"signalOptionsForm") + self.valueTypeLabel = QLabel(self.signalOptionsPage) + self.valueTypeLabel.setObjectName(u"valueTypeLabel") + + self.signalOptionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.valueTypeLabel) + + self.valueTypeCombo = QComboBox(self.signalOptionsPage) + self.valueTypeCombo.addItem("") + self.valueTypeCombo.addItem("") + self.valueTypeCombo.addItem("") + self.valueTypeCombo.addItem("") + self.valueTypeCombo.setObjectName(u"valueTypeCombo") + self.valueTypeCombo.setEditable(True) + + self.signalOptionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.valueTypeCombo) + + self.quantityLabel = QLabel(self.signalOptionsPage) + self.quantityLabel.setObjectName(u"quantityLabel") + + self.signalOptionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.quantityLabel) + + self.quantityCombo = QComboBox(self.signalOptionsPage) + self.quantityCombo.setObjectName(u"quantityCombo") + self.quantityCombo.setEditable(True) + + self.signalOptionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.quantityCombo) + + self.unitCombo = QComboBox(self.signalOptionsPage) + self.unitCombo.setObjectName(u"unitCombo") + self.unitCombo.setEditable(True) + + self.signalOptionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.unitCombo) + + self.unitLabel = QLabel(self.signalOptionsPage) + self.unitLabel.setObjectName(u"unitLabel") + + self.signalOptionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.unitLabel) + + self.rowsColumnsLabel = QLabel(self.signalOptionsPage) + self.rowsColumnsLabel.setObjectName(u"rowsColumnsLabel") + + self.signalOptionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.rowsColumnsLabel) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.rowsSpin = QSpinBox(self.signalOptionsPage) + self.rowsSpin.setObjectName(u"rowsSpin") + self.rowsSpin.setMinimum(1) + self.rowsSpin.setMaximum(9999) + + self.horizontalLayout.addWidget(self.rowsSpin) + + self.columnsSpin = QSpinBox(self.signalOptionsPage) + self.columnsSpin.setObjectName(u"columnsSpin") + self.columnsSpin.setMinimum(1) + self.columnsSpin.setMaximum(9999) + + self.horizontalLayout.addWidget(self.columnsSpin) + + + self.signalOptionsForm.setLayout(3, QFormLayout.ItemRole.FieldRole, self.horizontalLayout) + + self.descriptionLabel = QLabel(self.signalOptionsPage) + self.descriptionLabel.setObjectName(u"descriptionLabel") + + self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.descriptionLabel) + + self.descriptionEdit = QPlainTextEdit(self.signalOptionsPage) + self.descriptionEdit.setObjectName(u"descriptionEdit") + + self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.descriptionEdit) + + self.typeOptionsStack.addWidget(self.signalOptionsPage) + self.unsupportedTypePage = QWidget() + self.unsupportedTypePage.setObjectName(u"unsupportedTypePage") + self.unsupportedTypeLayout = QVBoxLayout(self.unsupportedTypePage) + self.unsupportedTypeLayout.setObjectName(u"unsupportedTypeLayout") + self.unsupportedTypeLabel = QLabel(self.unsupportedTypePage) + self.unsupportedTypeLabel.setObjectName(u"unsupportedTypeLabel") + self.unsupportedTypeLabel.setWordWrap(True) + + self.unsupportedTypeLayout.addWidget(self.unsupportedTypeLabel) + + self.typeOptionsStack.addWidget(self.unsupportedTypePage) + + self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.typeOptionsStack) self.portSplitter.addWidget(self.portDetailsPanel) @@ -129,7 +222,7 @@ class Ui_PortOptionsDialog(object): self.addPortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Add Port", None)) self.removePortButton.setText(QCoreApplication.translate("PortOptionsDialog", u"Remove Port", None)) self.nameLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Name:", None)) - self.typeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None)) + self.typeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Port type:", None)) self.typeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Signal", None)) self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None)) @@ -138,5 +231,16 @@ class Ui_PortOptionsDialog(object): self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None)) self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None)) + self.valueTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Type:", None)) + self.valueTypeCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"real", None)) + self.valueTypeCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"integer", None)) + self.valueTypeCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"boolean", None)) + self.valueTypeCombo.setItemText(3, QCoreApplication.translate("PortOptionsDialog", u"string", None)) + + self.quantityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Quantity:", None)) + self.unitLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Unit:", None)) + self.rowsColumnsLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Matrix size:", None)) + self.descriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None)) + self.unsupportedTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"This port type does not have an options editor yet.", None)) # retranslateUi diff --git a/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py index c7bb86b..455f10c 100644 --- a/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py +++ b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py @@ -78,8 +78,8 @@ class Ui_TextDefinitionEditor(object): self.portsLayout = QVBoxLayout(self.portsGroup) self.portsLayout.setObjectName(u"portsLayout") self.portsTable = QTableWidget(self.portsGroup) - if (self.portsTable.columnCount() < 4): - self.portsTable.setColumnCount(4) + if (self.portsTable.columnCount() < 10): + self.portsTable.setColumnCount(10) __qtablewidgetitem = QTableWidgetItem() self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem) __qtablewidgetitem1 = QTableWidgetItem() @@ -88,10 +88,22 @@ class Ui_TextDefinitionEditor(object): self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2) __qtablewidgetitem3 = QTableWidgetItem() self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3) + __qtablewidgetitem4 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(4, __qtablewidgetitem4) + __qtablewidgetitem5 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(5, __qtablewidgetitem5) + __qtablewidgetitem6 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(6, __qtablewidgetitem6) + __qtablewidgetitem7 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(7, __qtablewidgetitem7) + __qtablewidgetitem8 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(8, __qtablewidgetitem8) + __qtablewidgetitem9 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(9, __qtablewidgetitem9) self.portsTable.setObjectName(u"portsTable") self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.portsTable.setColumnCount(4) + self.portsTable.setColumnCount(10) self.portsLayout.addWidget(self.portsTable) @@ -120,18 +132,28 @@ class Ui_TextDefinitionEditor(object): self.parametersLayout = QVBoxLayout(self.parametersGroup) self.parametersLayout.setObjectName(u"parametersLayout") self.parametersTable = QTableWidget(self.parametersGroup) - if (self.parametersTable.columnCount() < 3): - self.parametersTable.setColumnCount(3) - __qtablewidgetitem4 = QTableWidgetItem() - self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4) - __qtablewidgetitem5 = QTableWidgetItem() - self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5) - __qtablewidgetitem6 = QTableWidgetItem() - self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6) + if (self.parametersTable.columnCount() < 8): + self.parametersTable.setColumnCount(8) + __qtablewidgetitem10 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem10) + __qtablewidgetitem11 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem11) + __qtablewidgetitem12 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem12) + __qtablewidgetitem13 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(3, __qtablewidgetitem13) + __qtablewidgetitem14 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(4, __qtablewidgetitem14) + __qtablewidgetitem15 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(5, __qtablewidgetitem15) + __qtablewidgetitem16 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(6, __qtablewidgetitem16) + __qtablewidgetitem17 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(7, __qtablewidgetitem17) self.parametersTable.setObjectName(u"parametersTable") self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.parametersTable.setColumnCount(3) + self.parametersTable.setColumnCount(8) self.parametersLayout.addWidget(self.parametersTable) @@ -173,20 +195,42 @@ class Ui_TextDefinitionEditor(object): ___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0) ___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None)) ___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1) - ___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None)) + ___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Port Type", None)) ___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2) ___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None)) ___qtablewidgetitem3 = self.portsTable.horizontalHeaderItem(3) ___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Multiple", None)) + ___qtablewidgetitem4 = self.portsTable.horizontalHeaderItem(4) + ___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value Type", None)) + ___qtablewidgetitem5 = self.portsTable.horizontalHeaderItem(5) + ___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Quantity", None)) + ___qtablewidgetitem6 = self.portsTable.horizontalHeaderItem(6) + ___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Unit", None)) + ___qtablewidgetitem7 = self.portsTable.horizontalHeaderItem(7) + ___qtablewidgetitem7.setText(QCoreApplication.translate("TextDefinitionEditor", u"Rows", None)) + ___qtablewidgetitem8 = self.portsTable.horizontalHeaderItem(8) + ___qtablewidgetitem8.setText(QCoreApplication.translate("TextDefinitionEditor", u"Columns", None)) + ___qtablewidgetitem9 = self.portsTable.horizontalHeaderItem(9) + ___qtablewidgetitem9.setText(QCoreApplication.translate("TextDefinitionEditor", u"Description", None)) self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None)) self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None)) self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None)) - ___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0) - ___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None)) - ___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1) - ___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None)) - ___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2) - ___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None)) + ___qtablewidgetitem10 = self.parametersTable.horizontalHeaderItem(0) + ___qtablewidgetitem10.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None)) + ___qtablewidgetitem11 = self.parametersTable.horizontalHeaderItem(1) + ___qtablewidgetitem11.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None)) + ___qtablewidgetitem12 = self.parametersTable.horizontalHeaderItem(2) + ___qtablewidgetitem12.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None)) + ___qtablewidgetitem13 = self.parametersTable.horizontalHeaderItem(3) + ___qtablewidgetitem13.setText(QCoreApplication.translate("TextDefinitionEditor", u"Quantity", None)) + ___qtablewidgetitem14 = self.parametersTable.horizontalHeaderItem(4) + ___qtablewidgetitem14.setText(QCoreApplication.translate("TextDefinitionEditor", u"Unit", None)) + ___qtablewidgetitem15 = self.parametersTable.horizontalHeaderItem(5) + ___qtablewidgetitem15.setText(QCoreApplication.translate("TextDefinitionEditor", u"Rows", None)) + ___qtablewidgetitem16 = self.parametersTable.horizontalHeaderItem(6) + ___qtablewidgetitem16.setText(QCoreApplication.translate("TextDefinitionEditor", u"Columns", None)) + ___qtablewidgetitem17 = self.parametersTable.horizontalHeaderItem(7) + ___qtablewidgetitem17.setText(QCoreApplication.translate("TextDefinitionEditor", u"Description", None)) self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None)) self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None)) pass diff --git a/BEdit/src/bedit/gui/simulation_window.py b/BEdit/src/bedit/gui/simulation_window.py index cbadc2f..a836ce5 100644 --- a/BEdit/src/bedit/gui/simulation_window.py +++ b/BEdit/src/bedit/gui/simulation_window.py @@ -280,7 +280,20 @@ class SimulationWindow(QMainWindow): items[path] = item parent = item parent.setData(0, Qt.ItemDataRole.UserRole, signal_name) - parent.setToolTip(0, signal_name) + metadata = self.results.signal_metadata.get(signal_name, {}) + unit = metadata.get("unit", "") + if unit: + parent.setText(0, f"{parts[-1]} [{unit}]") + tooltip = signal_name + details = [ + metadata.get("quantity", ""), + f"Unit: {unit}" if unit else "", + metadata.get("description", ""), + ] + details = [detail for detail in details if detail] + if details: + tooltip += "\n" + "\n".join(details) + parent.setToolTip(0, tooltip) parent.setFlags(parent.flags() | Qt.ItemFlag.ItemIsUserCheckable) parent.setCheckState(0, Qt.CheckState.Unchecked) tree.expandToDepth(0) @@ -334,7 +347,14 @@ class SimulationWindow(QMainWindow): (trace for trace in graph.traces if trace.name == signal_name), None ) if enabled and existing is None: - graph.traces.append(SimulationTrace(name=signal_name)) + metadata = self.results.signal_metadata.get(signal_name, {}) + graph.traces.append( + SimulationTrace( + name=signal_name, + y_label=metadata.get("quantity", ""), + unit=metadata.get("unit", ""), + ) + ) elif not enabled and existing is not None: graph.traces.remove(existing) page = self.ui.graphTabs.currentWidget() @@ -492,6 +512,7 @@ class SimulationWindow(QMainWindow): self.results.status.update(phase="Simulation finished", progress=10000) if isinstance(result, SimulationExecutionResult): self.results.data = result.data + self.results.signal_metadata = result.signal_metadata available_signals = set(result.data) for graph in self.results.graphs: graph.traces = [ @@ -499,6 +520,10 @@ class SimulationWindow(QMainWindow): for trace in graph.traces if trace.name in available_signals ] + for trace in graph.traces: + metadata = result.signal_metadata.get(trace.name, {}) + trace.unit = metadata.get("unit", "") + trace.y_label = metadata.get("quantity", "") if graph.x_axis not in available_signals: graph.x_axis = ( "time" @@ -570,14 +595,38 @@ class GraphWorkspacePage(QWidget): horizontal = x_values if x_values is not None else range(len(y_values)) sample_count = min(len(horizontal), len(y_values)) color = trace.properties.get("color") + unit = trace.unit or self.results.signal_metadata.get( + trace.name, {} + ).get("unit", "") + label = f"{trace.name} [{unit}]" if unit else trace.name self.axes.plot( list(horizontal)[:sample_count], y_values[:sample_count], - label=trace.name, + label=label, color=color if isinstance(color, str) and color else None, ) self.axes.set_title(self.graph.title) - self.axes.set_xlabel(self.graph.x_axis if x_values is not None else "sample") + if x_values is not None: + x_unit = self.results.signal_metadata.get( + self.graph.x_axis, {} + ).get("unit", "") + x_label = ( + f"{self.graph.x_axis} [{x_unit}]" + if x_unit + else self.graph.x_axis + ) + else: + x_label = "sample" + self.axes.set_xlabel(x_label) + units = { + trace.unit + or self.results.signal_metadata.get(trace.name, {}).get("unit", "") + for trace in self.graph.traces + if self.results.data.get(trace.name) is not None + } + units.discard("") + if len(units) == 1: + self.axes.set_ylabel(next(iter(units))) self.axes.grid(True, alpha=0.25) if self.axes.lines: self.axes.legend() diff --git a/BEdit/ui/parameter_options_dialog.ui b/BEdit/ui/parameter_options_dialog.ui index a16542a..be8d4ba 100644 --- a/BEdit/ui/parameter_options_dialog.ui +++ b/BEdit/ui/parameter_options_dialog.ui @@ -2,13 +2,222 @@ ParameterOptionsDialog - 00620380 - Parameter Options + + + 0 + 0 + 720 + 328 + + + + Parameter Options + - Qt::Orientation::HorizontalAdd ParameterRemove ParameterName:Type:Value: - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + Add Parameter + + + + + + + Remove Parameter + + + + + + + + + + + 3 + + + + + Name: + + + + + + + + + + Type: + + + + + + + true + + + + real + + + + + integer + + + + + boolean + + + + + string + + + + + + + + Matrix size: + + + + + + + + + 1 + + + 9999 + + + + + + + 1 + + + 9999 + + + + + + + + + + + + true + + + + + + + true + + + + + + + + + + Value: + + + + + + + Quantity: + + + + + + + Unit: + + + + + + + Description: + + + + + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + - buttonBoxaccepted()ParameterOptionsDialogaccept()buttonBoxrejected()ParameterOptionsDialogreject() + + + buttonBox + accepted() + ParameterOptionsDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + ParameterOptionsDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + + diff --git a/BEdit/ui/port_options_dialog.ui b/BEdit/ui/port_options_dialog.ui index 752b231..75afa9a 100644 --- a/BEdit/ui/port_options_dialog.ui +++ b/BEdit/ui/port_options_dialog.ui @@ -2,43 +2,288 @@ PortOptionsDialog - 00620380 - Port Options + + + 0 + 0 + 760 + 421 + + + + Port Options + - Qt::Orientation::Horizontal + + Qt::Orientation::Horizontal + - + + + - Add Port - Remove Port + + + + Add Port + + + + + + + Remove Port + + + - Name: - - Type: - Signal - Orientation: - InputOutput - Allow multiple connections - New ports start at (0, 0) in the icon editor.true + + 3 + + + + + Name: + + + + + + + + + + Port type: + + + + + + + + Signal + + + + + + + + Orientation: + + + + + + + + Input + + + + + Output + + + + + + + + Allow multiple connections + + + + + + + New ports start at (0, 0) in the icon editor. + + + true + + + + + + + + + + + Type: + + + + + + + true + + + + real + + + + + integer + + + + + boolean + + + + + string + + + + + + + + Quantity: + + + + + + + true + + + + + + + true + + + + + + + Unit: + + + + + + + Matrix size: + + + + + + + + + 1 + + + 9999 + + + + + + + 1 + + + 9999 + + + + + + + + + Description: + + + + + + + + + + + + + + This port type does not have an options editor yet. + + + true + + + + + + + - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + - buttonBoxaccepted()PortOptionsDialogaccept() - buttonBoxrejected()PortOptionsDialogreject() + + buttonBox + accepted() + PortOptionsDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + PortOptionsDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + diff --git a/BEdit/ui/text_definition_editor.ui b/BEdit/ui/text_definition_editor.ui index 409398a..9c2483e 100644 --- a/BEdit/ui/text_definition_editor.ui +++ b/BEdit/ui/text_definition_editor.ui @@ -40,14 +40,14 @@ Ports - QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection4NameTypeOrientationMultiple + QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection10NamePort TypeOrientationMultipleValue TypeQuantityUnitRowsColumnsDescription Add PortRemove PortQt::Orientation::Horizontal4020 Parameters - QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection3NameTypeValue + QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection8NameTypeValueQuantityUnitRowsColumnsDescription Add ParameterRemove ParameterQt::Orientation::Horizontal4020 diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index a59707f..17774fb 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -2,12 +2,12 @@ "format": "bedit-document", "version": 1, "metadata": { - "name": "Untitled" + "name": "Current Document" }, "roots": [ { - "id": "6060fdd6-e816-4da9-8bb6-fde1a66d3e92", - "name": "Test", + "id": "4014ba90-5538-4050-a9be-552318197ce1", + "name": "New Graph Block 1", "position": { "x": 0.0, "y": 0.0 @@ -52,9 +52,7 @@ } ] }, - "properties": { - "showName": false - }, + "properties": {}, "library": { "showSubtree": true }, @@ -63,10 +61,10 @@ "graph": { "blocks": [ { - "id": "2e6a192c-5aeb-45d6-80ee-d64a72392b76", + "id": "86ba2bf6-0da5-40a4-98e9-fb0d9c12c576", "name": "Constant0", "position": { - "x": -160.0, + "x": -224.0, "y": -160.0 }, "rotation": 0.0, @@ -87,7 +85,15 @@ } }, "type": "signal", - "multipleConnections": false + "multipleConnections": false, + "valueType": "real", + "quantity": "Length", + "unit": "m", + "dimensions": { + "rows": 1, + "columns": 1 + }, + "description": "" } ] }, @@ -96,7 +102,14 @@ "id": "parameter-022177fc", "name": "v", "type": "real", - "value": "2" + "value": "2", + "quantity": "", + "unit": "", + "dimensions": { + "rows": 1, + "columns": 1 + }, + "description": "" } ], "icon": { @@ -172,10 +185,10 @@ } }, { - "id": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0", - "name": "gain0", + "id": "7805cb2f-c938-44b2-a1f8-e1b4af13f7bf", + "name": "Gain0", "position": { - "x": -32.0, + "x": -96.0, "y": -160.0 }, "rotation": 0.0, @@ -195,7 +208,15 @@ } }, "type": "signal", - "multipleConnections": false + "multipleConnections": false, + "valueType": "real", + "quantity": "", + "unit": "", + "dimensions": { + "rows": 1, + "columns": 1 + }, + "description": "" } ], "outputs": [ @@ -213,7 +234,15 @@ } }, "type": "signal", - "multipleConnections": false + "multipleConnections": false, + "valueType": "real", + "quantity": "", + "unit": "", + "dimensions": { + "rows": 1, + "columns": 1 + }, + "description": "" } ] }, @@ -222,7 +251,14 @@ "id": "parameter-61a43a86", "name": "k", "type": "real", - "value": "2.5" + "value": "2.5", + "quantity": "Current", + "unit": "A", + "dimensions": { + "rows": 1, + "columns": 1 + }, + "description": "" } ], "icon": { @@ -277,676 +313,28 @@ "initialEquations": "" } } - }, - { - "id": "0b29c706-eb1b-4839-8ad2-bbed64efc01e", - "name": "const_and_time", - "position": { - "x": -160.0, - "y": -32.0 - }, - "rotation": 0.0, - "interface": { - "inputs": [], - "outputs": [ - { - "id": "port-ddc97277", - "name": "y", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 88.0, - "y": 40.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ] - }, - "parameters": [ - { - "id": "parameter-022177fc", - "name": "v", - "type": "real", - "value": "4.8" - } - ], - "icon": { - "shape": "rectangle", - "fill": "#f4f4f4", - "border": "#303030", - "text": "Text", - "size": { - "width": 128.0, - "height": 128.0 - }, - "elements": [ - { - "cornerRadius": 5.0, - "fill": "#f4f4f4", - "height": 64.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#303030", - "type": "rectangle", - "width": 64.0, - "x": 32.0, - "y": 32.0 - }, - { - "fill": "none", - "height": 48.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#00007f", - "type": "line", - "width": 0.0, - "x": 40.0, - "y": 40.0 - }, - { - "fill": "none", - "height": 0.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#00007f", - "type": "line", - "width": 48.0, - "x": 40.0, - "y": 88.0 - }, - { - "fill": "none", - "height": 0.0, - "lineStyle": "solid", - "lineWidth": 1.0, - "stroke": "#ffaa00", - "type": "line", - "width": 48.0, - "x": 40.0, - "y": 64.0 - } - ] - }, - "properties": { - "showName": true - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": "y = v+sin(10*time);", - "declarations": "", - "initialEquations": "" - } - } - }, - { - "id": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf", - "name": "gain1", - "position": { - "x": -32.0, - "y": -32.0 - }, - "rotation": 0.0, - "interface": { - "inputs": [ - { - "id": "port-df34ce84", - "name": "u", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 64.0, - "y": 64.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ], - "outputs": [ - { - "id": "port-fe9e6486", - "name": "y", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 88.0, - "y": 40.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ] - }, - "parameters": [ - { - "id": "parameter-61a43a86", - "name": "k", - "type": "real", - "value": "-5" - } - ], - "icon": { - "shape": "rectangle", - "fill": "#f4f4f4", - "border": "#303030", - "text": "Text", - "size": { - "width": 128.0, - "height": 128.0 - }, - "elements": [ - { - "cornerRadius": 5.0, - "fill": "#f4f4f4", - "height": 64.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#303030", - "type": "rectangle", - "width": 64.0, - "x": 32.0, - "y": 32.0 - }, - { - "color": "#00007f", - "fill": "#ffffff", - "fontSize": 24.0, - "height": 48.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#00007f", - "text": "K", - "type": "text", - "width": 48.0, - "x": 40.0, - "y": 40.0 - } - ] - }, - "properties": { - "showName": true - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": "y = k*u;", - "declarations": "", - "initialEquations": "" - } - } - }, - { - "id": "59bb51be-8185-4b0b-a894-e38db61aff18", - "name": "add0", - "position": { - "x": 96.0, - "y": -96.0 - }, - "rotation": 0.0, - "interface": { - "inputs": [ - { - "id": "port-c995845c", - "name": "u", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 64.0, - "y": 64.0 - } - }, - "type": "signal", - "multipleConnections": true - } - ], - "outputs": [ - { - "id": "port-a2f0dea6", - "name": "y", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 80.0, - "y": 48.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ] - }, - "parameters": [], - "icon": { - "shape": "rectangle", - "fill": "#f4f4f4", - "border": "#303030", - "text": "Text", - "size": { - "width": 128.0, - "height": 128.0 - }, - "elements": [ - { - "fill": "#f4f4f4", - "height": 48.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#303030", - "type": "circle", - "width": 48.0, - "x": 40.0, - "y": 40.0 - }, - { - "color": "#00007f", - "fill": "none", - "fontSize": 18.0, - "height": 32.0, - "lineStyle": "none", - "lineWidth": 1.5, - "stroke": "#00007f", - "text": "+", - "type": "text", - "width": 32.0, - "x": 48.0, - "y": 48.0 - } - ] - }, - "properties": { - "showName": false - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": "y = sum(u[i] for i in 1:$u_N$ );", - "declarations": "", - "initialEquations": "" - } - } - }, - { - "id": "78c8fcb6-bd46-4b85-aa91-3276a8d1e8cb", - "name": "Integrate0", - "position": { - "x": 224.0, - "y": -96.0 - }, - "rotation": 0.0, - "interface": { - "inputs": [ - { - "id": "port-df34ce84", - "name": "u", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 64.0, - "y": 64.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ], - "outputs": [ - { - "id": "port-fe9e6486", - "name": "y", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 88.0, - "y": 40.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ] - }, - "parameters": [ - { - "id": "parameter-61a43a86", - "name": "y_start", - "type": "real", - "value": "0" - } - ], - "icon": { - "shape": "rectangle", - "fill": "#f4f4f4", - "border": "#303030", - "text": "Text", - "size": { - "width": 128.0, - "height": 128.0 - }, - "elements": [ - { - "cornerRadius": 5.0, - "fill": "#f4f4f4", - "height": 64.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#303030", - "type": "rectangle", - "width": 64.0, - "x": 32.0, - "y": 32.0 - }, - { - "color": "#00007f", - "fill": "#ffffff", - "fontSize": 24.0, - "height": 48.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#00007f", - "text": "dt", - "type": "text", - "width": 48.0, - "x": 48.0, - "y": 40.0 - }, - { - "type": "text", - "x": 32.0, - "y": 32.0, - "width": 32.0, - "height": 56.0, - "fill": "none", - "stroke": "#00007f", - "lineWidth": 1.5, - "lineStyle": "none", - "text": "\u222b", - "fontSize": 35.0, - "color": "#00007f" - } - ] - }, - "properties": { - "showName": true - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": "der(y) = u;", - "declarations": "", - "initialEquations": "y = y_start;" - } - } - }, - { - "id": "f2caf59f-5688-46b4-b17d-bdc911580d7b", - "name": "Differentiate0", - "position": { - "x": 224.0, - "y": 32.0 - }, - "rotation": 0.0, - "interface": { - "inputs": [ - { - "id": "port-df34ce84", - "name": "u", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 64.0, - "y": 64.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ], - "outputs": [ - { - "id": "port-fe9e6486", - "name": "y", - "position": { - "x": 0.0, - "y": 0.0 - }, - "properties": { - "iconPosition": { - "x": 88.0, - "y": 40.0 - } - }, - "type": "signal", - "multipleConnections": false - } - ] - }, - "parameters": [ - { - "id": "parameter-61a43a86", - "name": "x_start", - "type": "real", - "value": "0" - }, - { - "id": "parameter-d110667b", - "name": "y_start", - "type": "real", - "value": "0" - }, - { - "id": "parameter-873b503d", - "name": "k", - "type": "real", - "value": "1" - }, - { - "id": "parameter-a55752c7", - "name": "T", - "type": "real", - "value": "0.01" - } - ], - "icon": { - "shape": "rectangle", - "fill": "#f4f4f4", - "border": "#303030", - "text": "Text", - "size": { - "width": 128.0, - "height": 128.0 - }, - "elements": [ - { - "cornerRadius": 5.0, - "fill": "#f4f4f4", - "height": 64.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#303030", - "type": "rectangle", - "width": 64.0, - "x": 32.0, - "y": 32.0 - }, - { - "color": "#00007f", - "fill": "#ffffff", - "fontSize": 22.0, - "height": 48.0, - "lineStyle": "solid", - "lineWidth": 1.5, - "stroke": "#00007f", - "text": "d/dt", - "type": "text", - "width": 64.0, - "x": 32.0, - "y": 40.0 - } - ] - }, - "properties": { - "showName": true - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": "assert(T > 0, \"Differentiate time constant T must be positive\");\n\nder(x) = (u - x)/T;\ny = (k/T)*(u - x);", - "declarations": "Real x(start=x_start);", - "initialEquations": "y = y_start;" - } - } } ], "connections": [ { - "id": "df23ff13-2a30-4c85-be66-56f66114d58e", + "id": "5582d4ad-7c7a-40fe-87d2-9b78410336b3", "source": { - "block": "2e6a192c-5aeb-45d6-80ee-d64a72392b76", + "block": "86ba2bf6-0da5-40a4-98e9-fb0d9c12c576", "port": "port-ddc97277" }, "target": { - "block": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0", + "block": "7805cb2f-c938-44b2-a1f8-e1b4af13f7bf", "port": "port-df34ce84" }, "name": "", "properties": { "waypoints": [] } - }, - { - "id": "00f7d291-641b-4200-afa3-f1ec941faed7", - "source": { - "block": "0b29c706-eb1b-4839-8ad2-bbed64efc01e", - "port": "port-ddc97277" - }, - "target": { - "block": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf", - "port": "port-df34ce84" - }, - "name": "", - "properties": { - "waypoints": [] - } - }, - { - "id": "7a776aab-deb5-4e50-838e-b6b4635f349c", - "source": { - "block": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0", - "port": "port-fe9e6486" - }, - "target": { - "block": "59bb51be-8185-4b0b-a894-e38db61aff18", - "port": "port-c995845c" - }, - "name": "", - "properties": { - "waypoints": [] - } - }, - { - "id": "5b3d71c3-282a-4c2d-85f8-b92bba5bf2df", - "source": { - "block": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf", - "port": "port-fe9e6486" - }, - "target": { - "block": "59bb51be-8185-4b0b-a894-e38db61aff18", - "port": "port-c995845c" - }, - "name": "", - "properties": { - "waypoints": [] - } - }, - { - "id": "33e15cc2-7f33-469e-91a8-921c04f6b823", - "source": { - "block": "59bb51be-8185-4b0b-a894-e38db61aff18", - "port": "port-a2f0dea6" - }, - "target": { - "block": "78c8fcb6-bd46-4b85-aa91-3276a8d1e8cb", - "port": "port-df34ce84" - }, - "name": "", - "properties": { - "waypoints": [] - } - }, - { - "id": "26e10b34-f168-4492-b4f2-fb46d8a4aafa", - "source": { - "block": "59bb51be-8185-4b0b-a894-e38db61aff18", - "port": "port-a2f0dea6" - }, - "target": { - "block": "f2caf59f-5688-46b4-b17d-bdc911580d7b", - "port": "port-df34ce84" - }, - "name": "", - "properties": { - "waypoints": [ - { - "x": 160.0, - "y": 96.0 - } - ] - } } ], "annotations": [], "junctions": [], - "simulation": { - "enabled": false, - "startTime": 0.0, - "stopTime": 10.0, - "intervalMode": "intervalTime", - "numberOfIntervals": 50000, - "intervalTime": 0.002 - } + "simulation": {} } } } diff --git a/m_Test.mo b/m_Test.mo index 0593840..232e8d5 100644 --- a/m_Test.mo +++ b/m_Test.mo @@ -6,7 +6,7 @@ model m_Test y = v; end m_Constant0; model m_gain0 - input Real u; + input Real u(quantity="Velocity", unit="m/s"); output Real y; parameter Real k = 2.5; equation @@ -16,7 +16,7 @@ model m_Test output Real y; parameter Real v = 4.8; equation - y = v+sin(time); + y = v+sin(10*time); end m_const_and_time; model m_gain1 input Real u; @@ -31,14 +31,43 @@ model m_Test equation y = sum(u[i] for i in 1:2 ); end m_add0; + model m_Integrate0 + input Real u; + output Real y; + parameter Real y_start = 0; + initial equation + y = y_start; + equation + der(y) = u; + end m_Integrate0; + model m_Differentiate0 + input Real u; + output Real y; + parameter Real x_start = 0; + parameter Real y_start = 0; + parameter Real k = 1; + parameter Real T = 0.01; + Real x(start=x_start); + initial equation + y = y_start; + equation + assert(T > 0, "Differentiate time constant T must be positive"); + + der(x) = (u - x)/T; + y = (k/T)*(u - x); + end m_Differentiate0; m_Constant0 Constant0; m_gain0 gain0; m_const_and_time const_and_time; m_gain1 gain1; m_add0 add0; + m_Integrate0 Integrate0; + m_Differentiate0 Differentiate0; equation gain0.u = Constant0.y; gain1.u = const_and_time.y; add0.u[1] = gain0.y; add0.u[2] = gain1.y; -end m_Test; \ No newline at end of file + Integrate0.u = add0.y; + Differentiate0.u = add0.y; +end m_Test;