Files
BondGraph/BEdit/AGENTS.md

18 KiB
Raw Blame History

AGENTS.md

This file is the working guide for AI coding agents contributing to BEdit. Its instructions apply to the entire repository.

Project purpose

BEdit is a Python/PySide6 graphical editor for hierarchical block and bond-graph models. A document contains reusable components, typed and oriented ports, connections, nested graphs, and editable vector icons.

The application is evolving quickly. Prefer small, coherent changes that improve the architecture without introducing compatibility layers unless compatibility is explicitly requested.

Architecture

The main boundary is strict:

src/bedit/
├── __main__.py
├── core/                   # Pure Python; must not import PySide6
│   ├── model.py            # Document, component, graph, port, icon data
│   ├── port_types.py       # Port type definitions and compatibility
│   ├── serializer.py       # JSON persistence
│   ├── libraries.py        # Library file discovery and parsing
│   └── simulation/         # Qt-free composition and OpenModelica interface code
└── gui/                    # All Qt-dependent code
    ├── app.py              # QApplication startup and palette
    ├── main_window.py      # Top-level UI orchestration
    ├── preferences.py      # Explicit disk-backed QSettings factory
    ├── controllers/        # Document controller and undo commands
    ├── dialogs/            # Dialog behavior
    ├── graphics/           # Workspace, icon editor, vector rendering
    ├── models/             # Qt tree models and repository adapters
    └── generated/          # Generated UI/resource Python; never hand-edit

Dependency direction:

  • bedit.core may depend only on the Python standard library.
  • bedit.gui may depend on bedit.core and PySide6.
  • bedit.core must never import bedit.gui or PySide6.
  • Data parsing and validation belong in core.
  • Signals, widgets, painting, Qt models, undo integration, and settings UI belong in gui.

Keep the root of src/bedit minimal. New UI modules should go into the relevant gui subpackage rather than the package root.

Important behavior and design decisions

  • Components may contain nested graph or text implementations.
  • Ports retain stable IDs. Display names, types, orientation, positions, and icon anchors may change without changing IDs.
  • Parameters belong to Component rather than a particular implementation kind, so graph and text components share stable-ID name/type/value records.
  • Components store one ordered ports list. Each port's orientation field is authoritative (input, output, or power-only indifferent); connection and presentation code derives any directional groupings when needed.
  • Port types are registered in core/port_types.py. Only compatible types may be connected. Signal ports connect by type; power ports additionally require the same domain. Editable power domains and causalities live in core/power_domains.py.
  • Power ports may use indifferent orientation and can act as either connection endpoint. For two indifferent ports, click order determines source/arrow direction; otherwise output/input semantics determine direction.
  • 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.
  • Connections store their port type and bond-graph causality explicitly. core/bond_graph.py owns causality inference; the simulation service invokes it before Modelica composition. The controller copies inferred values into the live model and refreshes the workspace before compile, export, or run. Causality values are none, source, target, warn_source, and warn_target.
  • Connection junctions are explicit typed graph objects. Splitting a connection creates one incoming and one outgoing segment; the junction can source further branches without overlapping full connection paths. Power bond connections cannot contain junctions.
  • Graph interaction has separate Pointer and Connect modes. Port hints are only visible in Connect mode. Connecting two blocks opens the compatible port-pair chooser; explicit port clicks determine its default selection. Connections use one freely angled polyline format with absolute properties.waypoints. Semantic ports choose compatibility, while each rendered endpoint is the intersection of the owning hitbox and its center-to-adjacent-route-point ray.
  • Connection appearance is configured per port type in gui/graphics/connection_styles.py, including color, width, pen style, and source/target arrowheads. Arrow styles are open, half, or filled. Do not scatter those constants through painters.
  • Graph annotations are box, line, or text objects in Graph.annotations. They use integer layers below or above graph layer 0. Annotation lines reuse connection polyline and absolute properties.waypoints semantics. Graph annotations and icon elements share ShapeOptionsDialog and shape_pen so their style fields and rendering must remain aligned.
  • Icon editing uses a fixed 128×128 coordinate space.
  • The visible/selectable component hitbox is calculated from vector elements, not from the complete 128×128 icon canvas.
  • New component icons default to a centered 64×64 shape.
  • Vector elements include rectangles, circles, ellipses, lines, triangles, and text. Shape styling and geometry are stored in document JSON.
  • Icon port anchors are stored in Port.properties["iconPosition"].
  • Workspace grid size, workspace snapping size, and icon grid size are separate persisted settings.
  • Settings use gui.preferences.application_settings() and are stored under the explicit BEdit/BEdit identity. Do not create anonymous QSettings() objects.
  • Avoid the QSettings group name general; Qt treats General specially in INI files. Autosave keys live under autosave/.
  • User-visible document edits should participate in undo/redo.
  • Component clipboard data is shared by the graph, document tree, and library tree. Pasting into the document node creates roots; pasting into a graph node creates children at an origin-normalized position. Always clone pasted trees with fresh IDs, preserve connections between jointly copied graph blocks, and assign unique sibling names.
  • The text-definition editor uses OpenModelica highlighting and completion from src/bedit/data/syntax/openmodelica.json. Keep keywords, types, built-ins, and named BEdit $name$ macro completions editable there; arbitrary $name$ expressions are highlighted as BEvalues. Highlight colors and bold/italic styles are persisted under syntax/<category>/ in application settings.
  • Text component sources may contain private Modelica declarations in source.declarations. The text-definition editor exposes declarations above source.initialEquations and source.equations, with the same highlighting and completion in all three fields. The composer emits each in its matching Modelica section.
  • The text-definition view embeds the same PortEditor and ParameterEditor implementations used by the standalone options dialogs. Do not reintroduce separate port/parameter tables in the text editor.
  • Application-wide messages use core.application_log.get_logger(). The main window installs the Qt log-panel handler; core code must only use standard Python logging and must not import the GUI handler.
  • File → Reload Simulation Code (Ctrl+F5) reloads modules under bedit.core.simulation, replaces the shared application/controller service, and preserves the previous instance attributes where possible.
  • Modelica composition lives in core/simulation/composer.py; the simulation service only owns application state and delegates composition. Ports with multipleConnections are emitted as Modelica arrays. Their size is inferred per component instance from graph connections and exposed while compiling as $portname_N$; array connection endpoints receive stable one-based indices in graph connection order.
  • Simulation → Export Model composes through Simulation.compose_source() without building the model, then writes the generated source as a .mo file.
  • OpenModelica integration belongs in core/simulation/openmodelica.py. Its persistent worker and OMC session start lazily on the first queued request. Never perform OMPython work directly on the Qt GUI thread. Result and error callbacks run on background threads and must use a Qt signal before touching UI. One lazy temporary working directory is shared by all requests in the session. Explicit application shutdown closes OMC and removes that directory plus the current session's OMPython log and port files; __del__ is only a fallback.
  • Simulation runs start an ephemeral localhost TCP listener before launching the generated model through OMC's system() function. OpenModelica's newline-delimited xmltcp status and message records are parsed in the core and forwarded through callbacks; the simulation service retains the latest progress for polling.
  • The application owns one reusable SimulationWindow. Starting a run clears its progress, log, and future result views. Extend graph presentation through its Designer-owned resultsLayout and the clear_result_views()/load_result_views() hooks.
  • Simulation-window geometry, dock/toolbar state, and central-results visibility persist under simulationWindow/ through application_settings().
  • Standalone simulation results are modeled in core/simulation/results.py. Its versioned schema retains model status, messages, metadata, and plottable traces so the simulation window can open results without an active document. Human-readable .json uses JSON, while the default .ber format uses the same compressed MessagePack approach as .beb documents.
  • After a successful OpenModelica run, <model>_res.csv is parsed on the worker before temporary-directory cleanup. SimulationResults.data stores every CSV column as a numeric array, including time, for later plotting and persistence.
  • The simulation window's dockable Signals tree derives hierarchy from dot-separated result-column names and bracketed array indices (a[1] becomes a → 1). Leaf items retain the exact full column name in UserRole; plotting code should consume SimulationWindow.selected_signal_names().
  • Simulation graph tabs persist as SimulationResults.graphs; every graph has a stable ID, editable title, and its own traces list. Runtime graph widgets belong in GraphWorkspacePage.plot_layout, not in the serialized core model. The Signals tree checkboxes edit the active graph's traces, and each page embeds a Matplotlib QtAgg canvas. Each graph persists its own x_axis signal (default time), selectable from the Signals tree context menu.
  • Embedded Matplotlib canvases provide cursor-centered wheel zoom and direct left-button drag panning without activating navigation-toolbar modes. Their custom navigation toolbar restores an explicit data-derived home view.
  • Rerunning the same composed model retains graph tabs, ordering, titles, X axes, and surviving trace settings while replacing numeric data. Missing signals are pruned from traces/X-axis selection and newly returned columns appear in the tree.
  • The optional OpenModelica executable is persisted as 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 <model>_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

Editable Designer sources are in ui/. Resources are defined in resources/resources.qrc.

Never hand-edit files in src/bedit/gui/generated/. Modify the corresponding .ui or .qrc source and regenerate instead.

Use the configured VS Code task Qt: Build Designer Files, or run the relevant commands directly:

pyside6-rcc resources/resources.qrc \
  -o src/bedit/gui/generated/resources_rc.py

pyside6-uic --from-imports ui/main_window.ui \
  -o src/bedit/gui/generated/ui_main_window.py

pyside6-uic --from-imports ui/settings_dialog.ui \
  -o src/bedit/gui/generated/ui_settings_dialog.py

pyside6-uic --from-imports ui/component_options_dialog.ui \
  -o src/bedit/gui/generated/ui_component_options_dialog.py

pyside6-uic --from-imports ui/port_options_dialog.ui \
  -o src/bedit/gui/generated/ui_port_options_dialog.py

pyside6-uic --from-imports ui/shape_options_dialog.ui \
  -o src/bedit/gui/generated/ui_shape_options_dialog.py

pyside6-uic --from-imports ui/icon_editor_dialog.ui \
  -o src/bedit/gui/generated/ui_icon_editor_dialog.py

pyside6-uic --from-imports ui/text_definition_editor.ui \
  -o src/bedit/gui/generated/ui_text_definition_editor.py

pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
  -o src/bedit/gui/generated/ui_simulation_settings_dialog.py

pyside6-uic --from-imports ui/simulation_window.ui \
  -o src/bedit/gui/generated/ui_simulation_window.py

pyside6-uic --from-imports ui/graph_parameters_dialog.ui \
  -o src/bedit/gui/generated/ui_graph_parameters_dialog.py

pyside6-uic --from-imports ui/parameter_options_dialog.ui \
  -o src/bedit/gui/generated/ui_parameter_options_dialog.py

When adding a promoted/custom widget in Designer, its header must use the real Python module path, for example bedit.gui.graphics.workspace.

Substantial windows and dialogs must have a Designer .ui source. Python classes bind behavior and data but must not reconstruct or replace those layouts at runtime. A tiny generic prompt with one field and OK/Cancel may remain code-only.

Editing conventions

  • Preserve stable document IDs and existing connections.
  • Validate candidate document changes before pushing an undo command.
  • Keep model serialization symmetrical: additions to to_dict() require matching handling in from_dict() and cloning where relevant.
  • Use descriptive domain names; avoid generic utils.py modules.
  • Prefer focused classes and helpers over growing main_window.py further.
  • Shared vector calculations that do not require Qt belong in core; Qt painter code belongs in gui/graphics.
  • Do not silently swallow malformed document data. Raise a useful ValueError in core, then present it through the GUI layer.
  • Preserve unrelated user changes. The worktree may already be dirty.
  • Do not delete or overwrite library/document JSON unless the requested workflow explicitly calls for it.

Document and library files

  • Documents use the bedit-document JSON format.
  • Documents can be stored as human-readable .bedit.json/.json through JsonDocumentSerializer, or as compressed MessagePack .beb through BebDocumentSerializer. UI document I/O dispatches via DocumentSerializer.
  • test.bedit.json is a useful manually created example during development.
  • Library documents use the same recursive document model.
  • Library parsing belongs in core/libraries.py; Qt change notifications belong in gui/models/library_repository.py.
  • Package library data, if present, belongs under src/bedit/data/libraries/.

Validation

There is not yet a complete automated test suite. For every change, run at least:

python3 -m compileall -q src
git diff --check

Run imports with the source tree explicitly available:

PYTHONPATH=src python3 -c "import bedit.core; import bedit.gui.app"

Verify the backend remains Qt-free after core changes:

PYTHONPATH=src python3 - <<'PY'
import sys
import bedit.core
assert not any(name.startswith("PySide6") for name in sys.modules)
PY

For model changes, add a focused in-memory round-trip check using GraphDocument.to_dict() and GraphDocument.from_dict(). For controller changes, exercise undo and redo when applicable.

GUI smoke tests may fail in headless environments because the system Qt platform theme tries to access a display even with an offscreen platform. Do not claim an interactive GUI test passed unless a display-capable environment was actually used. Compilation, imports, and pure model/controller checks remain useful.

If Ruff is installed, also run:

ruff check src

Running the application

Install the package in editable mode or run it from the source tree:

PYTHONPATH=src python3 -m bedit

The installed GUI entry point is bedit.gui.app:main. The first non-flag launch argument is opened as the initial document.

Completion checklist

Before handing off a change:

  1. Confirm the core/gui dependency boundary is intact.
  2. Confirm generated files were not hand-edited.
  3. Check serialization and cloning for model changes.
  4. Check undo/redo for document mutations.
  5. Run compilation and git diff --check.
  6. Report any GUI behavior that could not be tested interactively.
  7. Update README or this file if the architecture or workflow changed.